startActivity가 안돼요

startActivity 메소드(startActivityForResult 메소드 포함)는 파라미터로 전달된 인텐트(Intent) 정보로 액티비티를 실행한다. 안드로이드 책 처음마다 이 메소드 소개는 빠지지 않는다. 그렇게 많이 사용하는데, 왜 안될까?

확인이 쉬운 것부터 살펴보자. 액티비티 호출을 위해 만드는 인텐트 유형은 명시적 인텐트와 암시적 인텐트로 나뉜다. 인텐트를 잘 만들었는지 살펴보자.

  • 명시적 인텐트는 호출할 대상 패키지와 패키지 속 액티비티 정보를 넣어야 한다. 명시적 인텐트를 통해 호출을 시도했으나 에러가 난다면 패키지나 액티비티 이름이 틀린 것은 아닌지 확인해보자.
  • 암시적 인텐트는 액션(action)을 이용하므로, 액션이 틀렸는지 확인하자.

intent에 넣은 정보가 맞는데도 안되는 경우도 있다. 그럼, 다음 경우도 생각해 보자.

  • 암시적 인텐트를 사용할 때 액션과 함께 전달하는 추가정보가 잘못 되었는지 확인하자.  예를 들면, AndroidManifest.xml에 액티비티가 처리할 수 있는 URL 스키마를 선언할 수 있다. 이 설정과 전달하는 url 데이터가 맞지 않다면 실행되지 않는다.
  • 퍼미션이 필요한지 확인하자. 안드로이드의 컴포넌트는 자신을 호출하는 앱이 퍼미션을 갖고(granted)있어야 실행하도록 강제할 수 있다. 퍼미션이 없다면 Exception이 발생한다.
  • 액티비티가 앱 외부에서 공개되지 않게 설정되었을 수도 있다. AndroidManifest.xml 내의 액티비티 컴포넌트 선언부의 android:exported 어트리뷰트가 true인지 확인하자. 참고로, 기본값이 true다.
  • intent에 넣은 정보가 맞는데도 실행이 안된다면 앱이 설치되었는지 확인하자. 앱 슬롯이나 셋팅 내 어플이케이션 목록을 통해 확인할 수 있다.
  • 설치가 되었는데도 실행되지 않는다면 앱이 비활성화 됐을 수 있다. 셋팅 내 어플이케이션 목록에서 앱을 찾아, 사용하지 않기(비활성화)로 둔 것은 아닌지 확인해보자.
  • 혹시, MAIN 액션과 LAUNCHER 카테고리를 가지는 일명 메인 액티비티가 실행되지 않는다면 이 글을 읽어보자.

그 밖에

액티비티를 실행할 때는 가능하다면 암시적 인텐트를 이용하자.

  • 같은 액션을 활용하는 모든 앱이 사용자에게 표시되므로 사용자에게 선호하는 앱을 이용할 수 있게 선택권을 줄 수 있다.
  • 호출할 액티비티의 패키지 이름이 바뀌더라도 명시적으로 컴포넌트를 선언하지 않기 때문에, ActivityNotFoundException과 같은 문제가 발생하지 않는다.
  • 액티비티 정보는 쉽게 알 수 없으니 명시적 인텐트가 안전하다고 생각하는 경우가 있다. dumpstate의 설치된 앱 정보 등을 통해 액티비티를 알아낼 방법은 다양하므로, 호출은 모두 가능하다. 잘못된 값이 전달될 위험은 값을 validation하여 대응하고, 사용 자체를 막고자 한다면 퍼미션을 활용하자.

참고

메인 액티비티 실행이 안돼요

일반적으로 앱은 아이콘이 있다. 아이콘을 누르면 진입점 역할의 액티비티가 실행된다. 이 액티비티를 편의상 메인 액티비티라고 부르자.

메인 액티비티를 만드는 법은 간단하다. AndroidManifest.xml에 선언된 액티비티 중 메인으로 만들고 싶은 것을 골라 아래와 같이 MAIN 액션LAUNCHER 카테고리가 포함된 인텐트 필터를 추가하면 된다.

<intent-filter>
     <action android:name="android.intent.action.MAIN" /> 
     <category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

위와 같이 인텐트 필터를 선언하면, 런쳐는 메인 액티비티로 판단하여 앱 슬롯에 아이콘을 표시한다.

그럼, 코드로 메인 액티비티를 실행해보자. 내 앱이라면 패키지 이름과 컴포넌트 이름으로 명시적 호출도 가능하다. 여기서는 암시적 인텐트를 이용해 보자.

adb shell am start <패키지이름>

패키지 이름만 건네면 앱의 메인 액티비티를 찾아 실행하는 명령어다. 위 명령어로 앱들을 실행해보면 어떤 앱은 실행되고 어떤 앱은 실행되지 않는다. 왜 그럴까?

실행여부는 <intent-filter>에 android.intent.category.DEFAULT가 있어야 있느냐에 따라 달라진다. DEFAULT 카테고리가 있어야 패키지 이름만으로 실행할 수 있는 것이다.

숨겨진 액티비티도 아니고 아이콘이 노출된 앱인데 코드로 실행할 수는 없다니 이상하다. 게다가, 메인 액티비티는 공개된 액티비티이므로 android:exported=”true”도 강제라고 한다. 그럼 외부에서 실행하는 것을 보장하겠다는 뜻이 아닌가?

이런 앞뒤가 맞지 않는 상황 때문인지 안드로이드의 PackageManager 클래스는  API3부터 getLaunchIntentForPackage()라는 설명이 재미있는 API를 제공한다.

Returns a "good" intent to launch a front-door activity in a package. 
...(이하 생략)

메인 액티비티를 실행하는 “좋은” 인텐트를 만들어 반환한다고 API 설명이 되어 있다. 하지만, 이 API도 MAIN 액션, LAUNCHER 카테고리를 가진 액티비티를 찾아 반환하는 것이므로 여전히 인텐트 필터는 필요하다.

메인 액티지티를 만들 때 DEFAULT 카테고리도 잊지 말고 추가하도록 하자. 다른 앱이 내 앱을 한 번이라도 더 실행하도록 말이다.

그 밖에

  • 한 앱에서 MAIN, LAUNCHER 속성을 2개 이상의 액티비티에 동시에 선언할 수 있으며, 이 때 아이콘이 각각 생긴다.
  • 아이콘의 레이블은 액티비티의 레이블을 따라간다. 그러므로, 아이콘을 다르게 여러 개 만들 수도 있다.

참조

Fragment의 onActivityResult로 결과 받기

안드로이드 어플리케이션은 컴포넌트로 구성된다. ActivityManager는 컴포넌트들 사이에 Intent 형태의 데이터를 주고 받을 수 있게 해준다. 액티비티 간 데이터를 주고 받을 때 용이하도록 액티비티와 프레그먼트는 startActivity(), startActivityForResult(), onActivityResult() 메소드를 제공한다. 여기서는 프레그먼트의 onActivityResult()에 대해 살펴보자.

결론부터 말하면, 프레그먼트의 onActivityResult()도 다른 액티비티에서 데이터를 받을 수 있다. 다른 특별한 처리 없이 말이다.

지금 인터넷을 보면 액티비티의 onActivityResult()만 다른 액티비티나 프레그먼트에서 startActivityForResult()통해 요청한 작업의 결과를 받을 수 있다고 한다. 그리고, 프레그먼트의 onActivityResult()에 데이터를 전달하는 다양한 방법들(FragmentManager를 통한 프레그먼트의 onActivityResult() 명시적 호출, 이벤트 버스 사용 등)을 소개한다.  안드로이드가 이상하게 동작한다는 불만도 잊지 않는다.

아마도 이런 오해는 액티비티 간 데이터가 어떻게 전달되는지 이해하지 못해 발생한 것 같다. 액티비티가 데이터를 주고 받는 규칙을 살펴보자.

  • 액티비티의 onActivityResult()는 액티비티나 프레그먼트 둘 중 어느 것의 결과든 받는다.
  • 프레그먼트의 onActivityResult()는 프레그먼트의 startActivityForResult()를 통해 호출한 액티비티에서만 결과를 받는다.
  • 프레그먼트는 액티비티의 onActivityResult()가 호출 된 후 호출 된다. 정확히, 프레그먼트의 onActivityResult()는 액티비티의 onActivityResult() 코드 내부에서 호출된다.

그러므로, 아래와 같이 사용하면 결과를 받을 수 없다.

  • 프레그먼트 안에서 (예를 들면, getActivity() 같은 메소드를 통해 얻은) 액티비티 객체의 startActivityForResult()를 호출한다. 이 때는, 액티비티의 startActivityForResult()를 호출한 것이므로 액티비티로만 결과가 전달된다.
  • 내 액티비티에서 onActivityResult() 메소드를 오버라이드 한 후, 이 메소드에서 super.onActicityResult()로 결과를 전달하지 않고 데이터를 소진한다. 프레그먼트는 데이터를 액티비티의 onActivityResult()를 통해 전달받기 때문에 super.onActivityResult()로 값을 전달하지 않으면 프레그먼트에도 전달되지 않는다.

대신, 아래와 같이 사용해야 결과를 받을 수 있다.

  • Activity.startActivityForResult()가 아닌Fragment.startActivityForResult()를 사용해 호출한다.

프레그먼트 속 onActivityResult()로 결과가 오지 않아 힘들어 했다면, 지금이라도 바르게 사용해 보자.

그 밖에

  • 사실 액티비티에서 프레그먼트에 결과를 전달하는 과정은 인터넷에 알려진 FragmentManager를 이용한 자식 프레그먼트들의 onActivityResult() 명시적 호출과 크게 다르지 않다.
  • 프레그먼트를 추가할 때 건넨 tag를 이용해 프레그먼트를 찾기라도 한다면, 이 태그를 통해 액티비티와 프레그먼트가 강결합하게 되어 더 나쁘다.
  • 액티비티-프레그먼트A-프레그먼트B 순으로 포함하는 계층 구조라도, 프레그먼트 B에서 호출한 액티비티의 결과를 프레그먼트 A가 받지는 않는다. 액티비티에서 각 프레그먼트에 결과 값을 전달한다.
  • Fragment의 공식 문서를 보면 프레그먼트의 onActivityResult()에 대한 설명이 단촐하다. ‘액티비티의 onActivityResult()와 관련이 있다.’라고만 언급이 되어있다.
  • 액티비티의 onActivityResult()는 스코프(scope)가 protected이고, 프래그먼트의 그 것은 public이다.

참조