본문 바로가기
깡샘 코틀린

13-1 인텐트 이해하기

by 농농씨 2023. 6. 30.

01-02장 안드로이드 앱 개발 준비하기

03-05장 코틀린 이해하기

06-10장 앱의 기본 기능 구현하기

11-12장 구글의 라이브러리로 화면 구성하기를 거쳐왔다.

 

13~16장에서는 컴포넌트를 다룰 것이다. 안드로이드 앱은 액티비티, 브로드캐스트 리시버, 서비스, 콘텐츠 프로바이더 등 4개의 컴포넌트를 기반으로 개발한다. 지금까지 살펴본 액티비티를 제대로 개발하려면 인텐트의 원리와 액티비티의 생명 주기를 이해해야 한다. 그리고 액티비티 이외에 나머지 컴포넌트도 알아야 한다. 컴포넌트 작성 방법과 동작 원리를 알아볼 것이다.

 

13장의 주제는 액티비티이다. 액티비티는 안드로이드 앱에서 화면을 구성하는 컴포넌트이다. 액티비티의 화면 구성 방법 이외에 동작 방식 등을 구체적으로 알아야 제대로 액티비티를 이해하고 개발할 수 있다. 특히 이번 장의 '인텐트' 개념은 서비스나 브로드캐스트 리시버와도 연계되므로 잘 알아두도록 하자.

 

 

인텐트란?

안드로이드 앱은 모두 4개의 컴포넌트로 개발하는데 이때 핵심 클래스가 바로 Intent이다. 인텐트는 한마디로 '컴포넌트를 실행하려고 시스템에 전달하는 메시지'라고 정의할 수 있다. 즉, 기능을 수행하는 함수를 제공하는 클래스가 아닌 데이터를 담는 클래스이다. 이 데이터는 컴포넌트를 실행하는 정보이며 이 정보가 담긴 인텐트 객체를 시스템에 전달하면 컴포넌트가 실행된다.

 

인텐트가 무엇인지 알아보고자, 한 앱에 MainActivity와 DetailActivity가 있다고 가정해보자. MainActivity로 화면을 전환한다면 DetailActivity 클래스의 객체를 생성해서 실행하면 될 것 같다.

하지만 DetailActivity가 안드로이드의 컴포넌트 클래스라면 개발자가 코드에서 직접 생성해서 실행할 수 없다. 이미 02장에서 설명했듯 컴포넌트 클래스는 시스템이 생성해서 실행하는 클래스이므로 개발자가 작성하는 코드로 생명주기를 관리할 수 없다. 결국 MainActivity 클래스에서 DetailActivity 클래스를 실행하려면 시스템에 인텐트를 전달해 줘야 한다. 그러면 시스템에서 인텐트의 정보를 분석해서 그에 맞는 컴포넌트를 실행해 준다.

 

인텐트는 외부 앱의 컴포넌트와 연동할 때도 중재 역할을 한다. A 앱의 컴포넌트에서 인텐트를 시스템에 전달하고 그 정보를 분석하여 B 앱의 컴포넌트를 실행할 수 있다.

 

액티비티는 매니페스트 파일에 등록해야 한다. 액티비티 클래스 하나당 <activity> 태그 하나로 등록해야 하며, 이때 액티비티의 클래스 이름을 지정하는 name 속성은 생략할 수 없다.

// MainActivity와 DetailActivity 등록
<activity
	android:name=".DetailActivity" /> // name 속성 생략할 수 없음
<activity // 액티비티 하나당 <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>

액티비티뿐만 아니라 서비스, 브로드캐스트 리시버, 콘텐츠 프로바이더도 매니페스트 파일에 등록해야 한다. 이처럼 안드로이드 컴포넌트를 매니페스트 파일에 등록해야 하는 이유는 시스템에 컴포넌트를 알려야 하기 때문이다. 시스템은 런타임 때 매니페스트 파일의 정보를 참조하여 앱을 실행한다. 만약 어떤 컴포넌트를 개발해 놓고 매니페스트 파일에 등록하지 않으면 시스템은 해당 컴포넌트를 알 수 없다. 따라서 인텐트가 해당 컴포넌트를 실행할 수 없다.

MainActivity에서 DetailActivity를 실행하고자 인텐트를 시스템에 전달하는 코드는 다음과 같다.

// 인텐트를 시스템에 전달
val intent: Intent = Intent(this, DetailActivity::class.java)
startActivity(intent)

위 코드에서 startActivity() 함수가 인텐트를 시스템에 전달(시작)한다. 이 함수의 매개변수에는 시스템에 실행을 요청할 컴포넌트의 정보가 담긴 Intent 객체를 전달한다. 이 코드에서 Intent 생성자의 매개변수에 작성한 DetailActivity::class.java 는 클래스 타입 레퍼런스 정보이다. 즉, DetailActivity를 실행해달라는 정보를 Intent 객체에 대입한 것이다. (왜 즉,인지 모르겠네요)

 

인텐트 엑스트라 데이터

그런데 MainActivity에서 인텐트를 이용해 DetailActivity를 실행할 때 데이터를 전달해야 한다면?

DetailActivity에 함수 하나 선언하고 MainActivity에서 함수 호출하면서 매개변수로 데이터를 넘길 수도?➡️❌ 불가능!

컴포넌트 객체는 시스템이 생성하므로 개발자 코드로는 직접 접근할 수 없기 때문! 따라서 컴포넌트 객체로 어떤 함수를 호출할 수도 없음!

그럼 어떻게?

인텐트에 컴포넌트 실행을 요청할 때 데이터를 함께 전달하려면 엑스트라 데이터(extra data)를 이용해야 함. 엑스트라 데이터는 인텐트에 담는 부가 정보라 할 수 있음

MainActivity에서 인텐트에 엑스트라 데이터를 추가해서 전달하고 DetailActivity에서 인텐트에 담긴 엑스트라 데이터를 가져오는 구조.

이때 인텐트에 엑스트라 데이터를 추가하는 함수는 putExtra().

  • public Intent putExtra(String name, CharSequence value)

putExtra()의 첫번째 매개변수는 데이터 식별자, 두번째 매개변수는 전달할 데이터.

이 함수는 각 타입의 데이터를 담을 수 있도록 오버로딩*으로 선언되어 있음.

다음은 인텐트에 데이터를 추가하는 코드이다.

*오버로딩이란? 하나의 함수를 매개변수 타입 달라질 때 추가로 정의하지 않고 쓰던 거 쓰는 것.(함수 중첩)

// 엑스트라 데이터 추가
val intent: Intent = Intent(this, DetailActivity::class.java)
intent.putExtra("data1", "hello") // 문자열 데이터 추가함
intent.putExtra("data2", 10) // 정수 데이터 추가함
startActivity(intent)

 

인텐트로 실행된 컴포넌트에서 엑스트라 데이터 가져오려면? 인텐트 객체의 프로퍼티(intent)를 이용!

그리고 그 인텐트 객체의 getIntExtra() 함수로 데이터 가져오기~ 데이터 가져오는 함수도 타입별로 여러개임~

(여러 타입의 데이터를 지원하는건 데이터 추가(putExtra())와 데이터 가져오기(getIntExtra, getStringExtra 등) 둘 다 되지만 전자는 오버로딩 돼있고 후자는 데이터타입별로 구분되어있군)

  • public int getIntExtra(String name, int defaultValue)
  • public String getStringExtra(String name)
  • public double getDoubleExtra(String name, double defaultValue)
// 엑스트라 데이터 가져오기
val data1 = intent.getStringExtra("data1") // 인텐트의 엑스트라 데이터로부터 문자열 데이터 가져오기
val data2 = intent.getIntExtra("data2", 0) // Int 타입 데이터 가져오기

 

액티비티 화면 되돌리기

액티비티는 화면을 구성하는 컴포넌트임. 한 액티비티에서 인텐트로 다른 액티비티를 실행하면서 화면을 전환한 후에, 다시 돌아왔을때 사후 처리를 해야할수도, 안 할수도 있음. ex. 카톡 배경화면 바꾸기는 돌아왔을때 사후처리 해야함

사후처리여부에 따라 인텐트로 액티비티를 시작하는 방법은 다음 3가지이다.

  • public void startActivity(Intent intent) ➡️ 사후처리 필요 없을 때
  • public void startActivityForResult(Intent intent, int requestCode) ➡️ 사후처리 필요할 때(전통적 방법)
  • ActivityResultLauncher ➡️ 사후처리 필요할 때(안드로이드 11부터 최근에 권장되는 방법)

즉, 액티비티에서 무언가 실행하고 결과 받으려면 ActivityResultLauncher를 사용해라.

ex. 퍼미션 허용 여부 묻고 다이얼로그 결과 받을 때, 인텐트로 실행한 액티비티 결과 받을 때 등

 

먼저 전통적 방법인 startActivityForResult를 알아보자~

// 결과를 돌려받는 액티비티 시작
startActivityForResult(intent, 10)

액티비티를 startActivityForResult() 함수로 시작할 때 두 번째 매개변수는 개발자가 정하는 요청 코드(requestCode)이자 인텐트를 식별하는 값. 이 값은 이 함수로 결과를 돌려받은 후 별도로 처리할 때 필요함

 

다음 코드는 startActivityForResult() 함수로 시작한 액티비티에서 결과를 되돌리는 코드임. 사용자가 뒤로가기 버튼 안누르고 자동으로 화면 되돌리려면 finish() 함수(현재 화면에 보이는 액티비티를 종료해달라고 시스템에 요청하는 함수) 이용함.

// 결과와 화면 되돌리기
intent.putExtra("resultData", "world")
setResult(RESULT_OK, intent) // 결과 어떻게 되돌릴지 상수를 지정함
finish()

이러면 시스템이 이전 액티비티로 화면 되돌림. finish() 함수 호출하기 전에 결과 데이터를 인텐트 객체에 담을 수 있으며 이때 자신을 실행한 인텐트 객체에 엑스트라 데이터로 담으면 됨.

또한 setResult() 함수로는 결과를 어떻게 되돌릴지 지정 가능. 요청을 제대로 처리했으면 RESULT_OK, 아니면 RESULT_CANCELED 등 상수 지정하며 이 값을 결과코드(resultCode)라고 함.

// 결과를 돌려받은 후 처리(전환 전 화면)
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { 
// 결과 돌려받은 후 화면 전환돼서 자기자신 출력되면 자동으로 출력되는 함수
	super.onActivityResult(requestCode, resultCode, data)
    if (requestCode == 10 && resultCode == Activity.RESULT_OK) {
    	val result = data?.getStringExtra("resultData")
    }
}

위 코드는 처음 인텐트를 시작한 액티비티(전환하기 전 화면)에 작성되었음. 결과가 되돌아와서 다시 자신이 화면에 출력되면 onActivityResult() 함수가 자동으로 호출된다. 이 함수의 매개변수는 다음과 같다.

  • requestCode: 인텐트를 시작한 곳에서 인텐트를 구분하려고 설정한 요청 코드.
  • resultCode: 인텐트로 실행된 곳에서 돌려받은 결과 코드.
  • data: 인텐트 객체. 이 객체에 결과 데이터 있음.

그니까~ MainActivity에서 startActivityForResult(intent, 10)으로 인텐트로 액티비티를 시작하면, 

화면이 전환되고 DetailActivity 즉 시스템에 인텐트가 전달이 돼서,

DetailActivity 즉 시스템에서 putExtra로 인텐트에 데이터도 추가하고~ setResult로 결과를 어떻게 되돌릴지도 정하고~

마지막에 finish로 자동으로 액티비티 종료해달래서 처리도 해주고~ 요청을 처리해줌

그리고 액티비티 종료했으니까 다시 MainActivity로 화면이 전환됨!

이제 결과를 돌려받은 후 처리를 해야하니 onActivityResult() 함수가 자동으로 호출됨~

함수의 매개변수로는 어떤 걸 전달하는가 보면~

requestCode에는 인텐트 구분 위해 인텐트 시작지점에서 아까 10이라는 요청코드 받은거 그거 Int타입으로 넣고,

resultCode에는 인텐트 실행지점에서 setResult로 결과코드로 받은 상수 그거 Int 타입으로 넣고

그렇게 되더라~

 

 

액티비티 화면 되돌리기 - ActivityResultLauncher

이번에는 액티비티 화면전환 후 되돌아왔을 때, 최근에 권장되는 결과 사후처리 방법을 알아보자.

 

ActivityResultLauncher의 구조는 다음과 같다.

Contract(요청의 실행자) → ActivityResultLauncher(Contract와 Callback 등록) → launch(요청 발생)

(여기서 소괄호는 생성자 아님 주의)

 

Contract

ActivityResultLauncher를 이용하려면 먼저 Contract 객체가 필요함.

Contract는 ActivityResultLauncher로 실행될 요청을 처리하는 역할 즉, ActivityResultLauncher로 인텐트를 발생시켜 액티비티를 실행할 때 실제 인텐트를 발생시키는 역할을 함.

ex. 퍼미션 조정 요청할 때 실제 퍼미션 조정 다이얼로그 띄우는 역할

 

Contract는 ActivityResultContract를 상속받은 서브클래스이며 직접 만들거나 API에서 제공하는 클래스 이용하기도 가능.

대표적인 Contract는 다음과 같다.

  • PickContact: 선택한 연락처의 Uri* 획득
  • RequestPermission: 권한 요청, 허락 여부 파악
  • RequestMultiplePermissions: 여러 권한을 동시에 요청
  • StartActivityForResult: 인텐트 발생, 액티비티 실행 결과 획득
  • TakePicturePreview: 사진 촬영 후 비트맵 획득
  • TakePicture: 사진 촬영, 저장, 비트맵 획득

*URI는 특정 리소스를 식별하는 통합 자원 식별자(Uniform Resource Identifier)를 의미한다.

 

ActivityResultLauncher

ActivityResultLauncher는 registerForActivityResult() 함수로 만드는 객체이며 함수의 매개변수에 실제 작업자Contract 객체와 결과를 처리하는 Callback 객체를 등록해줌.

 

launch

ActivityResultLaunchr의 함수이며 launch 함수를 호출하는 순간 ActivityResultLauncher에 등록된 Contract 객체가 실행됨.

 

 

이제 실제 인텐트를 발생시켜 액티비티를 실행하고 결과를 처리하는 방법을 알아보자.

// ActivityResultLauncher 생성
val requestlauncher: ActivityResultLauncher<Intent> = registerForActivityResult(
    ActivityResultcontracts.StartActivityForResult()) // 이 한 줄은 Contract이다.
    		// StartActivityForResult()객체를 사용함
    {									
        val resultData = it.data?.getStringExtra("result")		// 중괄호 부분은 Callback
        binding.mainResultView.text = "result : $resultData"
    }

ActivityResultLauncher를 이용하려면 먼저 1️⃣registerForActivityResult() 함수로 ActivityResultLauncher 객체를 만들어야 함. 그리고 2️⃣registerForActivityResult() 함수에는 Contract와 Callback 객체를 등록해야 하며 여기서는 인텐트를 발생시키는 StartActivityForResult 객체를 사용함.

 

이렇게 만든 ActivityResultLauncher 객체를 launch() 함수로 실행해주면~ 끝~

// ActivityResultLauncher 실행
val intent: Intent = Intent(this, DetailActivity::class.java)
requestLauncher.launch(intent)

 

인텐트 필터

지금까지 인텐트를 시스템에 전달할 때 실행할 대상 컴포넌트 정보를 다음처럼 지정했다.

// 인텐트를 시스템에 전달
val intent: Intent = Intent(this, DetailActivity::class.java)

여기서는 대상 컴포넌트가 DetailActivity::class.java이다. 이 정보는 클래스 타입 레퍼런스이다.

같은 앱의 컴포넌트라면 이처럼 클래스 타입 레퍼런스로 설정할 수 있지만, 외부 앱의 컴포넌트는 이렇게 실행할 수 없다!

 

그래서 인텐트는 실행할 컴포넌트 정보를 어떻게 설정하는지에 따라 다음과 같이 2가지로 나뉨.

  • 명시적 인텐트(explicit intent): 클래스 타입 레퍼런스 정보를 활용한 인텐트. 내부 의 컴포넌트를 요청하는 인텐트 객체를 만들 때 사용함.
  • 암시적 인텐트(implicit intent): 인텐트 필터 정보를 활용한 인텐트. 클래스 타입 레퍼런스를 사용할 수 없는 외부 의 컴포넌트를 요청할 때 씀.

암시적 인텐트는 매니페스트 파일에 선언된 인텐트 필터를 이용한다.

// 암시적 인텐트
<activity android:name=".OneActivity" />
<activity android:name=".TwoActivity"
	android:exported="true">
    <intent-filter> // 인텐트 필터 사용해서 암시적 인텐트로 실행할 수 있음
    	<action android:name="ACTION_EDIT">
    </intent-filter>
</activity>

위 코드에서 OneActivity처럼 매니페스트 파일에 어떤 액티비티를 등록할 때 name 속성만 지정하면 해당 액티비티는 명시적 인텐트로만 실행할 수 있다

안드로이드 시스템은 매니페스트 파일의 내용을 참조하여 앱을 실행하는데, 개발자가 <activity android:name=".OneActivity" />처럼 액티비티의 클래스 이름만 설정하면 시스템에는 이 이름만 등록된다. 따라서 코드에서 인텐트에 클래스의 이름 정보(class type reference)를 지정해 줘야 등록된 정보와 일치하는지 비교해서 실행할 수 있다.

그러므로 앱 내부에서만 이용하는 컴포넌트라면 android:name 속성만 선언하면 된다. 그런데 어떤 컴포넌트를 외부에서도 인텐트로 실행할 수 있어야 한다면! 해당 컴포넌트가 있는 앱의 매니페스트 파일에 암시적 인텐트로 실행할 수 있게 <intent-filter>를 설정해줘야 한다.

 

<intent-filter>는 <activity>, <service>, <receiver> 등 컴포넌트 등록 태그 하위에 작성할 수 있다. <intent-filter> 태그는 컴포넌트를 암시적 인텐트로 실행할 때만 추가하며 반드시 사용할 필요는 없다. 인텐트 필터를 추가하면 해당 컴포넌트의 클래스명인텐트 필터 정보가 시스템에 등록된다. 그리고 어디선가 이 컴포넌트를 실행하려면 시스템에 등록된 정보에 맞게 인텐트를 설정한다.

 

❓내부 앱의 컴포넌트도 암시적 인텐트로 실행할 수 있나요?

❗️가능은 한데 내부앱은 인텐트 필터 설정 했든 안했든 클래스 타입 레퍼런스로 실행하는 것을 권장함. 내부 앱을 암시적으로 실행할 수 없을 때도 있음. 자세한 건 나중에.

❓외부 앱이랑 연동 안 하면 암시적 인텐트를 굳이 이용해야 하나요?

❗️대부분 앱이 외부 앱과 연동한다. 특히 구글이 제공하는 기본 앱과 연동한다!

 

인텐트 필터 하위에는 <action>, <category>, <data> 태그를 이용해 정보를 설정할 수도 있다. 개발자가 선택해서 하나만 선택할수도, 여러개를 함께 선언할 수도 있다. 각 태그에 설정하는 값은 다음의 의미와 같다.

  • <action>: 컴포넌트의 기능을 나타내는 문자열.
  • <category>: 컴포넌트가 포함되는 범주를 나타내는 문자열
  • <data>: 컴포넌트에 필요한 데이터 정보.

-<action> 태그

android:name에 설정하는 값: 개발자가 임의로 지정하는 문자열이며 앱에서 유일하지 않아도 됨, but 컴포넌트의 기능을 나타낼 것을 권장함. ex. 데이터 보여주는 기능 한다는 의미로 액션 문자열을 android.intent.actin.VIEW라고 선언한다든지~ 데이터 편집 기능 의미로 android.intent.action.EDIT라고 선언한다든지~

 

-<category> 태그

android:name에 설정하는 문자열: 컴포넌트가 어느 범주에 포함되어야 하는지를 의미. 개발자가 임의로 지정할 수도 있지만 대부분 플랫폼 API에서 제공하는 문자열을 이용함. ex. android.intent.category.LAUNCHER는 런처가 실행하는 컴포넌트라는 의미~ android.intent.category.BROSABLE은 브라우저가 실행하는 컴포넌트라는 의미~

 

-<data> 태그

컴포넌트에서 어떤 성격의 데이터를 처리하는지를 나타냄.

URL 형식으로 표현하며 <action>이나 <category>처럼 문자열 하나로 선언하지 않고 android:scheme, android:host, android:port, android:mimeType 등의 속성을 이용함. 이러한 속성은 필요한 만큼만 선언하면 됨.

  • scheme 속성: URL의 프로토콜명. 데이터의 성격을 한정지을 때 사용함.
  • host 속성: URL의 도메인.
  • port 속성: URL의 포트.
  • mimeType: 데이터 타입으로, text/plain이나 image/* 등을 선언할 수 있음.

 

인텐트 필터의 대표적인 예는 모듈을 만들 때 자동으로 만들어지는 MainActivity이다. 매니페스트 파일에 MainActivity는 다음처럼 등록되어 있다.

// 자동으로 만들어지는 메인 액티비티
<activity android:name=".MainActivity"
	android:exported="true" >
    <intent-filter>
    	<action android:name="android.intent.action.MAIN" /> // 인텐트 필터의 <action> 태그 정보
        <category android:name="android.intent.category.LAUNCHER" /> // 인텐트 필터의 <category> 태그 정보
    <intent-filter>
</activity>

 

MainActivity는 사용자가 런처 화면에서 앱의 아이콘 이미지를 터치했을 때 실행되는 액티비티이다. 그런데 앱의 아이콘이 나열된 런처 화면은 사실 런처 앱의 액티비티이다. 따라서 사용자가 아이콘을 터치해 앱을 실행하면 외부 앱(런처)에서 실행하는 것이므로 매니페스트 파일에 인텐트 필터를 등록해 암시적으로 실행되게 해야 한다.

런처 앱은 시스템에서 LAUNCHER 범주의 정보(<category>태그를 말하는듯)를 가져와 앱 아이콘을 나열한다. 따라서 앱의 첫 화면을 나타내는 액티비티에는 <action> 태그의 name 속성값을 android.intent.action.MAIN으로 선언하고, <category> 태그의 name 속성값은 android.intent.category.LAUNCHER로 선언한 인텐트 필터를 설정해야 한다.

 

앱의 첫 화면인 MainActivity의 인텐트 필터로 설명했지만 다른 컴포넌트도 외부 앱과 연동하려면 인텐트 필터를 등록해야함. 만약 TwoActivity를 다음처럼 등록했다면 인텐트 필터에 선언된 정보에 맞춰 액티비티를 시작해야 함.

// 외부 앱과 연동하는 인텐트 필터 설정(메인 액티비티 외)
<activity android:name=".TwoActivity"
	android:exported="true">
    <intent-filter>
    	<action android:name="ACTION_EDIT" /> // <action> 태그는 기능 나타내므로 아마도 이 인텐트는 편집기능
        <category android:name="android.intent.category.DEFAULT" />
        <data android:scheme="http" />
    </intent-filter>
</activity>

 

다음은 인텐트의 action과 data 프로퍼티에 실행 대상인 컴포넌트 정보를 지정하는 코드이다. 이때 컴포넌트 정보는 매니페스트에 선언한 정보를 이용한다. data값은 URL문자열인데 안드로이드 URL 문자열은 Uri 객체로 표현한다.

// 인텐트의 프로퍼티를 이용하는 방법
val intent = Intent()
intent.action = "ACTION_EDIT"
intent.data = Uri.parse("http://www.google.com")
startActivity(intent)

 

앞의 코드는 action, data 정보를 인텐트의 각 프로퍼티에 설정한 것이며 다음처럼 생성자의 매개변수로 지정해도 된다.

// 인텐트의 생성자를 이용하는 방법
val intent = Intent("ACTION_EDIT", Uri.parse("http://www.google.com"))
startActivity(intent)

 

그런데 매니페스트 파일에는 <category> 태그의 name 속성값을 android.intent.category.DEFAULT로 선언했는데 인텐트 정보에는 카테고리 정보를 설정하지 않았다. 이처럼 인텐트에 카테고리 정보를 전달하지 않으면 기본이 DEFAULT 값으로 지정된다. 물론 매니페스트 파일에서 카테고리 정보에 DEFAULT가 아닌 다른 문자열을 지정했다면 인텐트에도 해당 문자열을 그대로 지정해야 한다.

 

다음은 <data> 태그의 mimeType 속성값을 설정한 예이다.

// mimeType 설정
<activity android:name=".TwoActivity">
    <intent-filter>
    	<action android:name="ACTION_EDIT" />
        <category android:name="android.intent.category.DEFAULT" />
        <data android:mimeType="image/*" /> // 데이터 태그 주목~
    </intent-filter>
</activity>

이렇게 설정하면 이를 실행하는 인텐트에도 타입 정보를 설정해 줘야 한다.

// 타입 정보 설정
val intent = Intent("ACTION_EDIT")
intent.type = "image/*" // 인텐트 실행시킬 때도 그대로 써야함~
startActivity(intent)

(인텐트는 약간... 주문서 같은 게 아닐까? A액티비티가 B액티비티를 실행해달라고 요청하는 주문서를 시스템에 의뢰하는거지~)

 

액티비티 인텐트 동작 방식

만약에 인텐트로 실행할 액티비티가 시스템에 없다면? 또는 실행할 액티비티가 1개 이상이라면?

명시적 인텐트는 클래스 타입 레퍼런스 정보를 이용하므로 액티비티가 없거나 여러 개일 수 없다.

그런데 암시적 인텐트실행할 액티비티를 <action>, <category>, <data> 등의 문자열 정보로 나타내므로 없거나 여러 개일 수 있다.

다음은 실행할 액티비티 개수에 따라 시스템이 어떻게 처리하는지를 나타낸다.

  • 없을 때: 인텐트를 시작한 곳에 오류가 발생함.
  • 1개일 때: 문제없이 실행.
  • n개일 때: 사용자 선택으로 하나만 실행(ex. 지도앱 여러개 중에 하나 골라서 실행하기)

다음 코드는 인텐트로 실행할 액티비티의 정보를 잘못 지정한 예이다.

// 해당 액티비티가 없을 때
val intent = Intent("ACTION_HELLO")
strtActivity(intent)

이처럼 실행할 액티비티가 없으면 다음과 같은 오류가 뜬다.

android.content.ActivityNotFOUNDException: No Activity found to handle Intent {
act=ACTION_HELLO}

 

따라서 인텐트로 실행할 액티비티가 없을 수도 있는 상황을 고려해서 다음처럼 예외 처리를 해줘야 한다.

// 해당 액티비티가 없을 때 예외 처리
val intent = Intent("ACTION_HELLO")
try {
	startActivity(intent)
} catch (e: Exception) {
	Toast.makeText(this, "no app...", Toast.LENGTH_SHORT).show() // 토스트는 화면 하단에 잠깐 뜨는 짧은 메시지!
}

 

인텐트가 여러개면 사용자가 선택한 하나만 실행된다.

// 액티비티가 여러 개일 때
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("geo:37.7749, 127.4149"))
startActivity(intent)

그런데 사용자의 폰에 위와 같은 정보로 등록된 지도 앱이 여러개면 선택할 수 있는 다이얼로그가 떠서 사용자에게 묻고 선택한 액티비티를 실행한다.

 

만약 앞에서처럼 액티비티가 여러 개 있더라도 사용자에게 묻지 않고 특정 앱의 액티비티를 실행하고 싶다면 해당 앱의 패키지명을 지정하면 된다. 다음 코드는 setPackage() 함수로 실행할 앱의 패키지를 지정한 예이다. 패키지는 앱의 식별자이므로 이 식별자가 가리키는 앱의 액티비티가 실행된다.

// 패키지 지정하기
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("geo:37.7749, 127.4149"))
intent.setPackate("com.google.android.apps.maps") // 실행하고 싶은 앱의 패키지명을 지정하기
startActivity(intent)

 

 

패키지 공개 상태

안드로이드 11(API 레벨 30) 버전부터는 앱의 패키지 공개 상태(package visibility)를 지정하지 않으면 외부 앱의 패키지(앱의 식별자) 정보에 접근할 수 없게 되었다. 이 내용은 지금까지 살펴봤던 인텐트를 이용해 외부 앱과 연동하는 부분에는 영향이 없으며, 외부 앱을 연동하더라도 패키지 정보를 활용하지 않는다면 아무런 문제가 없다.

다만 다음과 같은 함수를 사용할 때는 패키지 공개 상태에 따라 영향을 받는다.

  • PackageManager.getPackageInfo()
  • PackageManager.queryIntentActivities()
  • Intent.resolveActivity()
  • PackageManager.getInstalledPackages()
  • Packagemanager.getInstalledApplications()
  • bindService()

bindService()를 제외하고는 모두 외부 앱의 정보에 접근할 때 사용하는 함수이다. 외부 앱의 정보를 가져오거나 폰에 설치된 모든 앱의 정보를 얻거나 인텐트로 실행할 컴포넌트가 있는지 판단할 때 사용한다. bindService()는 서비스 컴포넌트와 관련된 함수이고 외부 앱의 서비스를 연동할 떄 패키지 공개 상태의 영향을 받는다. 이와 관련된 내용은 이후 서비스 컴포넌트를 다루는 15장에서 자세히~

 

다음은 PackageManager의 getPackageInfo() 함수를 이용해 매개변수에 지정한 패키지 문자열로 식별되는 앱의 정보를 가져오는 코드이다.

// 외부 앱의 정보를 가져오는 코드
val packageInfo = packagemanager.getPackageInfo("com.example.text_outer", 0)
val versionName = packageInfo.versionName

이 코드는 안드로이드 10버전까지 별 문제 없다가 11버전부터 오루가 발생한다. 안드로이드 11 버전 이상이 설치된 폰에 적용된 패키지 공개 상태 필터링 때문이다. 오류 없이 정상으로 실행하려면 매니페스트 파일에 외부 앱의 정보에 접근하겠다고 다음과 같이 선언해줘야 한다.

// 특정 외부 앱에 접근 선언
<manifest ... 생략 ... >
    <queries> // 외부 앱의 정보에 접근하겠다고 선언함
    	<package android:name="com.example.text_outer">
    </queries>
    (... 생략 ...)
</manifest>

<queries> 태그 하위에 <package> 태그를 이용해 접근하고자 하는 앱의 패키지명을 선언하고 실행하면 오류 없이 앱의 정보에 접근할 수 있다.

만약 여러 앱에 접근해야 할 때는 <package> 태그를 여러 번 선언하면 되는데, <queries> 태그를 사용하지 않고 모든 외부 앱의 정보에 접근할 수 있도록 허용해달라는 의미로 다음처럼 <uses-permission>을 선언할 수도 있다. 이렇게 선언하고 테스트해도 외부 앱의 정보에 접근할 수 있지만 될 수 있으면 <queries> 태그를 이용할 것을 권장한다.

// 모든 외부 앱에 접근 선언
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />

 

 

 

공부일기

더보기

음... 양이 너무 많아서 지루해지지 않으려고 빠르게빠르게 막 줄여썼는데 그것도 쓰다보니 결국 다 내가 모르는 내용이라 잘 안줄여지네

그래도 벌써 700쪽 중에 400쪽! 오늘 밤 새버릴까~ 내일 다 끝내버리면 우짜지~ ㅋㅋㅋ 라고 할뻔... 응 전혀 못끝냄 머리아파