그러냐

안드로이드 Service 에서 Activity 를 실행하는 방법 본문

android

안드로이드 Service 에서 Activity 를 실행하는 방법

관절분리 2016. 5. 26. 13:23
반응형
<안드로이드 Task 는 저를 늘 곤혹스럽게 만들곤 합니다.>

 안드로이드에서 어플리케이션을 개발하다보면 코드 상으로는 될 거같지만, 막상 실행 시켜보면 오류가 발생하는 경우가 있습니다. Service 를 하나 만들어, 인터넷에서 음악 파일을 다운 받고, 파일 다운로드가 종료되면 뮤직 플레이어를 실행시키는 일도 그런 일 중에 하나입니다. 안드로이드 플랫폼 상에서는 Activity 가 아닌 Service 혹은 BroadcastReceiver 에서 Activity 를 하나 새롭게 생성하려고 할 경우에는 다음과 같은 생소한 예외를 만나게 됩니다. 
ERROR/AndroidRuntime(): Calling startActivity() from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag. Is this really what you want?
 간단히 살펴보면, Activity 가 아닌 곳에서 startActivity()를 호출 하지 말고, 만일 호출하고자 할 때면 FLAG_ACTIVITY_NEW_TASK 를 사용하라는 말입니다. 그리고 마지막에는 왠지 협박조로, 그게 정말 너가 원하는거냐? 라고 끝을 맺네요. 

  그런데 어째서 이런 오류가 발생하는 것일까요? 

 안드로이드 플랫폼에서 어플리케이션의 UI 가 어떻게 구성되어지는지 조금 살펴볼 필요가 있습니다. 안드로이드 어플리케이션 UI의 핵심 요소는 두 가지 Activity 와 Task 입니다. Activity 는 개별 개별의 화면을 나타낸다고 생각할 수 있고, 어플리케이션의 진행에 따라 서로 상호작용하는 Activity 가 차곡 차곡 쌓여있는 것을 Task 라고 할 수 있습니다. 용어가 좀 헷갈리네요. 안드로이드 개발자 사이트에서 Task 에 관한 내용을 살펴보면, "Stack 형태(Last In First Out)로 배치된 서로 연관된 Activity 의 그룹" 이라고 되어 있습니다. 결국 기능적으로는 'Activity Stack' 이라고 생각하셔도 될 듯 합니다. 

<카드 놀이를 상상하면, 안드로이드 Activity 와 Task 를 이해하기 쉽습니다.>

 윈도우에 깔려있는 두번째로 훌륭한 어플리케이션 (첫번째는 지뢰찾기) 카드놀이를 상상해보면 좀 더 이해하기 쉽습니다. 한장의 카드는 하나의 Activity 입니다. 여러장의 카드가 뒤섞여져 있는 카드 뭉치는 실행 전의 Activity 들이라고 생각할 수 있습니다. 그리고 화면 하단, 여러장의 카드가 일정한 규칙을 가지고 쌓여있는 덱은 Task 라고 상상할 수 있습니다. 카드 놀이에서 새로운 카드를 한장 뽑아 덱에 추가하는 일은 다음의 과정을 거칩니다.
  1. 우선 카드 뭉치에서 카드를 한장 선택합니다. 
  2. 기존에 카드가 놓인 덱에는 규칙에 따라 카드를 놓습니다.
  3. 비어있는 덱에는 아무 조건 없이 카드를 추가할 수 있습니다.

 

잘못된 비유는 안하는 것만 못할 때가 있는 거 같습니다 ㅠㅠ;;; 카드놀이에서 빈 덱에 카드를 추가하기 위해서는 K 카드만이 가능하네요. 지적해주신 나비님 감사합니다~~! 카드게임에 대한 마지막 비유는 잊어 주시고;;; '안드로이드에서 새로운 TASK 를 생성하는데는 별다른 조건이 필요하지 않고, 그저 카드 한장(즉 Activity 하나) 만이 필요할 뿐이다.' 점만 기억해 주세요~~

 

새로운 Activity 가 Task 에 추가되는 것도 거의 동일합니다.

  1. 우선 실행하고자 하는 Activity 를 선택합니다. (Intent 를 통해)
  2. 특정 규칙에 부합한다면, 이미 존재하는 Task 에 Activity 를 추가합니다.
  3. 새로운 Task를 생성한다면 아무 조건없이 Activity 를 추가 할 수 있습니다.
 카드놀이에서 기존의 덱에 카드를 추가하기 위한 조건이 '무늬의 색깔이 달라야 하고, 숫자가 작아야 한다' 라고 한다면, 새로운 Activity 는 이미 Task 에 존재하는 다른 Activity 가 해당 Activity 를 실행 시키는 경우에, 기존 Task 에 추가 됩니다. (Activity 의 속성 중, allowTaskReparenting 값에 의해 달라질 수도 있습니다...)

  결국, 오류가 발생하는 원인은 굉장히 단순 합니다. Activity 를 Launch 시키기 위해서는 어떤 Task 위에 해당 Activity 를 추가할지 알아야 합니다. 그런데 Service 는 어떠한 Task 에도 속하지 않습니다. (오직 Activity 만이 Task 로 관리됩니다.) 따라서 Service 에서 Intent 를 날려 안드로이드 ActivityManagerService 에 Activity 를 "Activity 실행시켜 주세요" 라고 말한다면, 안드로이드 시스템은 다음과 같이 대답하게 됩니다. "응? 어디다가 실행시켜달란 말이냐? 혹시 완전 새로운 Task 에다가 실행시켜 주랴? 그렇다면 확실하게 FLAG_ACTIVITY_NEW_TASK 플래그를 써서 알려달라. 오바"

 그럼 이 문제는 어떻게 해결 할 수 있을까요? 두 가지 방법이 있습니다. 
 
 첫번째, 오류 발생 시 알려주는 내용대로 FLAG_ACTIVITY_NEW_TASK 플래그를 사용하면 됩니다. 만일 실행 시키려고 하는 Activity 가 이미 작동중인 Task 가 있다면 해당 Task 에, 그렇지 않다면 아예 새로운 Task 에 Activity 를 위치 시키라는 뜻입니다.
Intent i = new Intent(this, ServiceTest.class);
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(i);
 두번째로 PendingIntent 를 사용 하는 방법이 있습니다. PendingIntent 는 '위임된 Intent' 로 만일 Service 를 시작시키는 Activity 가 해당 Service 에 타겟 Activity 를 Launch 시키기 위한 PendingIntent 를 생성해서 Service 에 넘겨주게 되면, Service 는 적절한 시점에 전달받은 PendingIntent 를 send 할 수 있습니다. 이 경우, 실재 안드로이드 플랫폼에 Intent 를 전달하는 Sender 는 권한을 위임받은 Service 가 아니라 Service 에 권한을 위임한 Activity 가 되는 셈임으로 별 문제없이 새로운 Activity 를 시작할 수 있습니다. NotificationManager 에서 주로 사용하고 있는 방식입니다. 

 그런데 개발자들이 조금 헷갈리게도, PendingIntent 를 사용해 Service 에서 Activity 를 실행 시킬 수 있는 또다른 방법이 있습니다. 바로 Service 에서 직접 PendingIntent 를 선언해서 사용하는 방법입니다.
Intent i = new Intent(this, ServiceTest.class);
PendingIntent p = PendingIntent.getActivity(this, 0, i, 0);
try {
p.send();
} catch (CanceledException e) {
e.printStackTrace();
}
 어라? 이 코드는 어째서 작동하는 것일까요? PendingIntent 를 만든다고 안드로이드 플랫폼에서 어떻게 Service 에서 요청한 Activity 시작 명령을 처리해 줄 수 있는걸까요? 해답은 고양이(LogCat)가 알고 있습니다. 로그를 살펴보니, PendingIntent 의 경우에는 ActivityManager 가 강제로 FLAG_ACTIVITY_NEW_TASK 를 추가해 준다고 하네요.
 
  잘못된 PendingIntent 때문에 발생하는 오류의 경우, 실재 죄를 지은 사람(Intent 생성자)과 죄에 인해 피해를 보는 사람(Intent 송신자)이 다릅니다. 때문에, 그저 Intent 를 전달하는 일만 부탁받은 죄없는 Component 가 괜히 잘못을 덤탱이 쓰지 않도록 안드로이드 플랫폼은 PendingIntent 를 사용할 때 발생하는 오류(예를 들어 적절한 Activity 를 찾을 수 없거나 권한이 없어서 발생하는 오류) 를 모두 조용하게(강제 종료 팝업 없이) 처리해 줍니다. 

 역시 안드로이드는 멋진 녀석입니다. 
반응형