티스토리 뷰

3장에서는 애플리케이션 구성요소인 액티비티, 서비스, 브로드캐스트 수신자에 대해 소개를 하면서 화면을 여러 개 만들어서 화면 간 전환하는 방법을 설명하고, 화면을 전환하기 위한 '인텐트' 객체에 대해 나온다. 이번 포스팅에서는 액티비티 위주로 정리를 하고, 서비스와 브로드캐스트 수신자에 대해 간단하게 정리해 보려고 한다. 추가적으로 위험 권한을 설정하는 방법까지 알아볼 것이다.

 

[화면 구성과 화면 간 전환]

애플리케이션 구성요소

안드로이드 애플리케이션 구성요소는 다음과 같이 4가지가 있다.

  • 액티비티(Activity) - 눈에 보이는 화면

  • 서비스(Service) - 눈에 보이지 않는 동작

  • 브로드캐스트 수신자(Broadcast Receiver) - 핸드폰에서 수신한 SMS 문자를 브로드캐스팅 하면 브로드캐스트 수신자가 이를 받아서 처리한다.

  • 내용 제공자(Content Provider) - 핸드폰 앨범의 사진을 앱이 가져와서 쓰려면 보안 때문에 이미지를 그대로 가져다 쓸 수 없다. 그 대신 앨범에 있는 내용 제공자를 이용해 이미지를 가져와 사용할 수 있다.

위 네 가지 구성요소들의 클래스는 안드로이드 SDK에서 제공하며, 안드로이드 시스템에서 관리한다. 그렇기 때문에 이 구성요소를 추가하거나 변경할 경우 매니페스트(AndroidManifest.xml) 파일에 어떤 구성요소가 추가 또는 변경되었는지를 넣어두어야 앱이 설치될 때 시스템이 이 파일을 보고 구성요소를 확인할 수 있다.

 

 

액티비티 전환하기

시스템이 액티비티를 관리하기 때문에 화면을 띄우거나 전환할 때에도 시스템이 관리를 한다.

그렇기 때문에 액티비티를 전환하기 위해서는 시스템이 알아 볼 수 있는 형식인 Intent를 사용해야 한다.

다른 액티비티를 호출하는 과정

화면을 전환하는 코드는 다음과 같다.

Intent intent = new Intent(getApplicationContext(), MenuActivity.class);
startActivity(intent);

 

화면 전환 후 다시 돌아올 때 응답 받을 수도 있는데 그 코드는 다음과 같다.

Intent intent = new Intent(getApplicationContext(), MenuActivity.class);
startActivityForResult(intent, 101);    
// 여기서 101은 여러 화면이 있을 때 어느 화면에서 응답을 받을 것인지를 구분하는 구분자 역할.

 

참고로 화면 전환 시 액션바의 타이틀을 바꾸고 싶을 때는 AndroidManifest.xml에서 다음 코드를 넣으면 가능하다.

<activity
	android:name=".NameOfActivity"
	android:label="액션바 타이틀" />

 

 

액티비티 끝내기

액티비티를 끝내기 위해서는 finish() 함수를 사용한다.

액티비티를 만들게 되면 스택 형태로 관리하기 때문에 finish()를 통해 해당 액티비티를 종료하면 이전 실행 액티비티가 실행 액티비티가 된다.

액티비티 스택과 액티비티의 동작 과정

 

액티비티 끝내기 전에 결과를 반환하고 싶으면 아래와 같이 intent.getExtra()에 인자로 key-value 튜플 형태의 번들 데이터를 넣으면 그 값을 넘겨줄 수 있다.

Intent intent = new Intent();
intent.getExtra(“name”, “mike”);
setResult(Activitiy.RESULT_OK, intent);    // Activity.RESULT_OK는 정상 응답 임을 의미하는 상수
finish();

 

호출 한 액티비티에서 응답을 받기 위해서는 onActivityResult() 메서드를 오버라이딩 해야 한다.

// requestCode(요청 코드): 화면 전환할 때 설정한 101, 102와 같은 구분자
// resultCode (응답 코드): 화면 끝낼 때 설정한 Activity.REUSLT_OK와 같은 상수
// data: 화면 끝낼 때 전달 받은 intent 데이터

protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
    super.onActivityResult(requestCode, resultCode, data);

    if (requestCode = 101) {
        String name = data.getStringExtra(“name”);    // “mike” 리턴
    }
}

 

 

 

[인텐트]

인텐트는 액티비티, 서비스, 브로드캐스트 리시버 사이의 데이터를 주고 받는 형태의 객체이다.

 

 

인텐트 생성자

  • Intent() - 빈 인텐트 객체 생성

  • Intent(Intent o) - 인텐트 객체와 동일한 인텐트 객체 생성

  • Intent(String action [, Uri uri]) - ACTION_~~로 시작하는 액션을 넣어 다음에 취할 액션을 담은 인텐트 객체 생성

  • Intent(Context packageContext, Class<?> cls) - 호출할 클래스 객체를 지정한 인텐트 객체 생성

  • Intent(String action, Uri uri, Context packageContext, Class<?> cls)

 

 

명시적 인텐트 vs 암시적 인텐트

  • 명시적 인텐트

    • 인텐트에 클래스 객체나 컴포넌트 이름을 지정하여 호출할 대상을 확실히 알 수 있는 경우

  • 암시적 인텐트

    • 액션과 데이터를 지정하긴 했지만 호출할 대상이 달라질 수 있는 경우

    • MIME 타입에 따라 안드로이드 시스템에서 적절한 다른 앱의 액티비티를 찾은 후 띄워준다.

    • 액션과 데이터로 구성되지만 그 외에도 여러 가지 속성을 가지고 있다.
      ex) 범주(Category), 타입(Type), 컴포넌트(Component), 부가 데이터(Extra Data), ...

 

 

액티비티 전환 메서드

  • startActivity(Intent intent)

    • 새로 띄우는 다른 액티비티로부터 받는 응답을 처리할 필요가 없을 때 간단하게 사용

  • startActivityForResult(Intent intent, int requestCode)

    • 새로 띄우는 다른 액티비티로부터 응답을 받고자 할 때 사용

    • requestCode를 통해 데이터를 선택적으로 주고 받는다.

    • 위의 그림처럼 액티비티B 에서는 setResult()를 호출해서 응답을 보내고, 액티비티A 에서는 onActivityResult()를 통해 응답을 받는다.

 

 

플래그

위에서 언급했듯이 액티비티는 ‘액티비티 스택(Activity Stack)’으로 관리된다. 그렇기 때문에 새로운 액티비티를 띄우면, 기존의 액티비티는 스택에 저장되고 새로운 액티비티가 화면에 보이게 되는 구조이다. 이때, 동일한 액티비티가 여러 개 스택에 들어가게 되고 동시에 데이터를 여러 번 접근하거나 리소스를 여러 번 사용하는 문제가 발생할 수 있는데 이런 경우에 사용하는 것이 플래그(flag)이다.

 

플래그는 intent.addFlags()메서드를 사용하면 되는데 인자로 들어갈 플래그들은 대표적으로 다음과 같다.

 

  • FLAG_ACTIVITY_SINGLE_TOP

    • 액티비티 스택의 TOP에 동일한 액티비티가 존재한다면 해당 액티비티를 새로 만들지 않고 이전에 만든 액티비티를 그대로 사용한다.

      예를들어 액티비티A에서 어떠한 이유로 다시 액티비티A를 호출하는 경우, 새로운 액티비티A가 만들어져 실행되고 액티비티 스택의 TOP은 액티비티A가 된다. 여기서 FLAG_ACTIVITY_SINGLE_TOP 플래그를 적용하면 액티비티A를 새로만들지 않고 원래 만들어져 있던 액티비티A를 재사용하는 것이다.

    • 액티비티 재사용 시 onCreate()가 호출되지 않기 때문에 onCreate() 안에서 getIntent()를 이용해 인텐트를 받는 코드는 실행되지 않는다. 이런 경우에는 onNewIntent() 메서드를 재정의하면 이 메서드에서 인텐트 객체만 전달 받아 처리할 수 있다.

  • FLAG_ACTIVITY_CLEAR_TOP

    • 기존 액티비티 스택에 존재하는 (메모리에 올라와 있는) 액티비티를 호출할 때, 액티비티를 새로 만들지 않고 이전에 만든 액티비티를 그대로 사용한다. 이때 이전에 만든 동일한 액티비티 위에 쌓여있는 모든 액티비티를 모두 없앤다.

  • FLAG_ACTIVITY_NO_HISTORY

    • 액티비티[A]에서 다음 액티비티[B]를 호출할 때, NO_HISTORY 플래그를 추가하면 그 다음 액티비티[B]는 스택에 액티비티가 추가되지 않는다.

    • 만약 다음 액티비티[B]가 또 다른 액티비티[C]를 실행한 경우, 액티비티 스택에는 [C]만 쌓이지만 [B]의 종료시점은 [C]가 종료되는 시점에 [B]도 함께 종료된다.

  • FLAG_ACTIVITY_REORDER_TO_FRONT

    • 동일한 액티비티가 스택 안에 있으면 해당 액티비티를 스택의 TOP으로 가져온다.

  • FLAG_ACTIVITY_NEW_TASK

    • 서비스나 브로드캐스트 수신자와 같이 화면이 없는 구성요소에서 액티비티를 호출할 때 이 플래그를 통해 태스크를 생성해 주어야 한다.

 

 

부가 데이터

intent에 부가 데이터를 넣어서 전달할 때, putExtra()메서드를 사용해 key-value 튜플 형태로 데이터를 넣게 된다. 이때 value에는 원시 자료형(Primitive Data Type, ex. int, float, string, …)이 들어가야 하는데, 원시 자료형이 아닌 사용자 정의 객체(ex. Person 클래스)를 전달하고자 할 때는 전달하고자 하는 객체의 클래스가 Serializable / Parcelable 이 두가지 중 하나의 인터페이스를 구현한 클래스여야 한다.

 

Parcelable 인터페이스가 Serializable보다 좀 더 메모리 용량을 적게 차지하기 때문에 Person과 같이 직접 정의하는 객체들은 Parcelable 인터페이스를 구현한 후 부가데이터로 추가하는 것을 권장한다. ArrayList와 같은 객체들은 이미 Serializable 인터페이스를 구현하고 있으므로 그대로 부가데이터로 추가할 수 있다. 하지만 ArrayList<?>에 ?가 원시 자료형이 아니라면 해당 클래스도 Serializable 인터페이스를 구현하고 있어야 한다.

 

putExtra()로 넣은 데이터는 getStringExtra(), getIntExtra(), getSerializableExtra(), getParceableExtra()와 같은 형태의 메서드를 사용해 추출할 수 있다.

 

참고로 Serializable과 Parcelable의 차이점은 다음과 같다.

Serializable은 자바 표준 직렬화 인터페이스인데 이는 마커 인터페이스(Marker Interface)이기 때문에 별 다른 함수를 구현해야 하지 않아도 된다. 하지만 그 대신에 직렬화 과정에서 안드로이드에서는 필요하지 않은 많은 추가 오브젝트가 생성되고, 이로 인해 Garbage Collection이 많이 일어나면서 성능이 저하될 수 있다. 

Parcelable은 안드로이드 SDK로 제공하는 직렬화 인터페이스이다. 안드로이드를 위해 특별히 만들어진 인터페이스이기 때문에 성능은 좋지만 인터페이스의 함수를 구현해야 하는 댓가가 필요하다. 하지만 안드로이드 전용 인터페이스이기 때문에 대개 Parcelable이 안드로이드에서는 많이 사용되고 있다고 한다.

여기에 Serializable과 Parcelable을 비교한 좋은 글이 있는데 함께 읽어보면 좋을 것 같다. 해당 글의 마지막에서는 Serializable과 Parcelable이 큰 차이가 없으니 어느 것을 사용할지 너무 고민하지 말라고 한다.

 

 

 

액티비티 생명 주기

액티비티 생명주기는 다음 그림과 같다.

 

 

데이터의 복구

액티비티 수명주기 메서드가 자동 호출되도록 만든 이유는 사용자가 입력했던 데이터를 복구하거나 상태 정보를 복구할 수 있도록 만들기 위해서이다. 따라서 화면이 없어질 때 데이터를 임시로 저장해두었다가 화면이 다시 보일 때 복구할 수 있어야 하는데 이때 사용되는 메서드가 onPause()와 onResume()이다.

 

onPause() 메서드는 액티비티가 정지되기 직전(ex. 액티비티 스택에 들어가기 직전)에 호출된다. 이때 저장해야 하는 변수의 값이 있지만 이를 디스크나 데이터베이스에 저장하는 것은 번거로울 경우 SharedPreferences를 통해 간단하게 값을 저장할 수 있다. 아래 코드는 그 예시로 "mike"라는 문자열을 임시로 저장하는 코드이다.

SharedPreferences pref = getSharedPreferences("pref", Activity.MODE_PRIVATE); 
SharedPreferences.Editor editor = pref.edit(); 
editor.putString( "name", "mike"); 
editor.commit();

 

onResume() 메서드는 액티비티가 다시 포커스 될 때(ex. 액티비티 스택에 있던 액티비티가 스택의 TOP이 되어 화면에 보여질 때) 호출된다. 여기서 onPause()에서 저장한 값을 SharedPreferences를 통해 간단하게 저장한 데이터를 복원할 수 있다. 아래 코드는 그 예시로 저장했던 "mike"라는 문자열을 복원하는 코드이다.

SharedPreferences pref = getSharedPreferences("pref", Activity.MODE_PRIVATE); 
if ( (pref != null) && (pref.contains("name")) ) { 
	String name = pref.getString("name", "");  // 두 번째 인자는 default value를 의미
    nameInput.setText(name); 
}

 

 

이 방법 이외에도 onSaveInstanceState() 메서드와 onRestoreInstanceState() 메서드를 사용하는 방법도 있다.

onSaveInstanceState() 콜백 메서드를 재정의하면 아래 코드와 같이 outState라는 번들(Bundle)객체가 만들어지는데, 이 객체에 데이터를 저장할 수 있다.

@Override
public void onSaveInstanceState(Bundle outState) {
   outState.putString("message", "This is my message to be reloaded");
   super.onSaveInstanceState(outState);
}

 

그러면 이 콜백 메서드가 액티비티가 중지되기 전에 호출된다. 이때 데이터를 저장한 번들 객체는 액티비티가 다시 만들어질 때 호출되는 onCreate()나 화면에 다시 보일 때 호출되는 onRestoreInstanceState() 메서드의 파라미터로 전달돼서 그 전달된 파라미터로 원래 데이터를 복구할 수 있다. 데이터는 아래 코드와 같이 복구할 수 있다.

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ...
    
    if (savedInstanceState != null) {
        String message = savedInstanceState.getString("message");
    }
}
@Override
protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {   
  	super.onRestoreInstanceState(savedInstanceState);
       
    String message = savedInstanceState.getString("message");
}

 

 

 

서비스

서비스는 화면이 없는 시스템이다. 서비스는 시스템이 관리하고, 서비스가 종료된다고 하더라도 시스템이 자동으로 재시작시킨다. 서비스의 생명주기는 아래 그림과 같다.

서비스의 생명주기

 

서비스가 시스템에 의해 자동으로 재시작되어 startService()를 호출할 필요가 없다고 생각할 수 있지만, 액티비티에서 서비스로 데이터를 전달 하려고 하는 경우에도 startService() 메서드를 사용해야 한다. startService()의 파라미터로 Intent 객체를 넣어 데이터를 전달하는 것이다. 

서비스에서 데이터를 전달 받는 경우는 유의할 점이 있다. 서비스의 onCreate() 메서드는 서비스가 이미 생성되어 있을 때는 호출되지 않기 때문에 부가 데이터를 전달 받고자 할 때는 onCreate() 뿐만 아니라 onStartCommand() 메서드를 재정의해서 이 메서드에서도 getIntent()를 통해 데이터를 전달 받을 수 있도록 해야 한다.

 

 

반대로 서비스에서 액티비티로 데이터를 전달하고자 하는 경우는 Intent를 만들어 startActivity()에 인자로 넣어주면 된다. 아래 코드는 서비스에서 메인 액티비티를 띄우면서 "mike"라는 문자열을 전달하는 코드이다.

Intent showIntent = new Intent(getApplicationContext(), MainActivity.class);
showIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);
showIntent.putExtra("name", "mike");

startActivity(showIntent);

위에서 언급했지만, 화면이 없는 서비스에서 화면이 있는 액티비티를 띄울 때는 태스크(Task)를 새로 만들어서 연결해야 한다. 이 때문에 FLAG_ACTIVITY_NEW_TASK 플래그를 추가하게 되는데 일반적인 경우 FLAG_ACTIVITY_SINGLE_TOP과 FLAG_ACTIVITY_CLEAR_TOP까지 해서 세 개의 플래그를 같이 사용한다.

액티비티가 화면에 보인 상태에서 위와 같이 startActivity를 호출하면 FLAG_ACTIVITY_SINGLE_TOP 플래그에 의해 액티비티는 새로 만들어지지 않고 기존 액티비티를 그대로 사용한다. 그렇기 때문에 메인 액티비티에서 onCreate()가 아닌 onNewIntent() 메서드가 자동으로 호출되기 때문에 이 메서드 안에서 데이터를 전달 받아 처리해야 한다.

 

 

 

 

브로드캐스트 수신자

안드로이드에서 브로드캐스팅(Broadcasting)이란 메시지를 여러 객체에 전달하는 것을 말한다. 예를 들어, 문자를 받았을 때 이 문자를 SMS 수신 앱에 알려줘야 한다면 브로드캐스팅으로 전달하면 되는 것이다. 이런 메시지 전달 방식을 '글로벌 이벤트(Global Event)'라고 부른다. 글로벌 이벤트의 대표적인 예로는 전화나 문자와 같은 사용자 알림이 있다.

 

자신이 만든 앱에서 브로드캐스팅 메시지를 받고 싶다면 브로드캐스트 수신자(Broadcast Receiver)를 만들어 앱에 등록하면 된다. 원래는 새로운 애플리케이션 구성요소를 만들면 매니페이스 파일에 등록을 해야 하지만 브로드캐스트 수신자는 소스 코드에서 registerReceiver() 메서드를 사용해 시스템에 등록할 수 있다.

 

먼저 BroadcaseReceiver를 상속받은 SmsReceiver 클래스를 만들자. 그러면 onRecieve() 메서드를 재정의해야 한다. 이 메서드는 원하는 브로드캐스트 메시지가 도착하면 자동으로 호출되는데, 시스템의 모든 메시지를 다 받을 수 없기 때문에 매니페스트 파일에 <intent-filter>에 SMS 문자만 받도록 필터를 시스템에 등록해야 한다.

<receiver
    android:name=".SmsReceiver"
    android:enabled="true"
    android:exported="true">
    <intent-filter>
        <action android:name="android.provider.Telephony.SMS_RECEIVED" />
    </intent-filter>
</receiver>

 

아래 코드는 SmsReceiver 클래스의 onReceive() 메서드에서 SMS 수신 시 발신자, 내용, 수신 시각을 뽑아내는 코드이다. 마지막의 sendToActivity() 메서드는 직접 정의한 메서드로 수신 받은 SMS 내용을 액티비티에 전달하는 기능을 하는데 뒤에서 설명할 것이다.

Bundle bundle = intent.getExtras();	// 인텐트에서 Bundle 객체 가져오기
SmsMessage[] messages = parseSmsMessage(bundle);

if (messages != null && messages.length > 0) {
    String sender = messages[0].getOriginatingAddress();                // 발신번호 가져오는 함수
    String contents = messages[0].getMessageBody();                     // 내용 가져오는 함수
    Date receivedDate = new Date(messages[0].getTimestampMillis());     // 시각 가져오는 함수
    
    sendToActivity(context, sender, contents, receivedDate);
}

 

위 코드에서 parseSmsMessage() 메서드는 직접 정의한 메서드로 브로드캐스트 메시지를 SmsMessage[] 자료형으로 반환하는 기능을 한다. 이 메서드는 한 번 입력해 놓으면 다른 앱을 만들 때도 재사용 할 수 있다. 왜냐하면 SMS 데이터를 확인할 수 있도록 안드로이드 API에 정해둔 코드를 사용하기 때문에 수정될 일이 거의 없기 때문이다.

private SmsMessage[] parseSmsMessage(Bundle bundle) {
    Object[] objs = (Object[]) bundle.get("pdus");
    SmsMessage[] messages = new SmsMessage[objs.length];

    for(int i = 0; i < objs.length; ++i) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            String format = bundle.getString("format");
            messages[i] = SmsMessage.createFromPdu((byte[]) objs[i], format);
        }
        else messages[i] = SmsMessage.createFromPdu((byte[]) objs[i]);
    }

    return messages;
}

인텐트 객체 안에 부가 데이터로 들어 있는 SMS 데이터를 확인하려면 SmsMessage 클래스의 createFromPdu() 메서드를 사용하여 SmsMessage 객체로 변환하면 SMS 데이터를 확인할 수 있다. 이때 Build.VERSION.SDK_INT는 단말의 OS 버전을 확인할 때 사용하는데, Build.VERSION_CODES.M은 마시멜로 버전을 의미한다. 그렇기 때문에 위의 코드는 해당 단말의 OS 버전이 마시멜로 버전보다 높은 경우에만 format을 추가 파라미터로 넣는 코드가 된다.

 

그런데 앱에서 SMS를 수신하려면 RECEIVE_SMS라는 권한이 있어야 하기 때문에 매니페스트 파일에 해당 권한을 등록해 주어야 한다. 하지만 이 권한은 위험권한으로 분류되기 때문에 위험 권한에 대한 처리가 필요하다. 이에 대해서는 뒤에서 설명할 것이다.

<uses-permission android:name="android.permission.RECEIVE_SMS" />

 

이제는 수신 받은 SMS 내용을 액티비티에 전달하여 나타내려고 한다. 위에서 SmsReceiver 클래스의 onReceive() 메서드의 코드에서 보았던 sendToActivity() 메서드를 정의해야 한다. SmsReceiver 클래스의 아래 코드를 추가한다.

private void sendToActivity(Context context, String sender, String contents, Date receivedDate){
    Intent intent = new Intent(context, MainActivity.class);

    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
    intent.putExtra("sender", sender);
    intent.putExtra("contents", contents);
    intent.putExtra("receivedDate", format.format(receivedDate));

    context.startActivity(intent);
}

액티비티에 SMS 내용을 전달하기 위해서는 인텐트를 만든 뒤 인텐트에 부가 데이터를 넣고 startActivity()로 액티비티를 호출해야 한다. 브로드캐스트 수신자도 화면이 없는 구성요소이므로 액티비티로 인텐트를 전달하기 위해서는 FLAG_ACTIVITY_NEW_TASK로 새로운 태스크를 만들어야 한다.

 

이제 메인 액티비티에서는 이를 받아서 화면에 표시할 수 있다.

Intent intent = getIntent();

if(intent != null) {
    sender.setText(intent.getStringExtra("sender"));
    contents.setText(intent.getStringExtra("contents"));
    receivedDate.setText(intent.getStringExtra("receivedDate"));
}

 

지금까지 만든 브로드캐스트 수신자의 동작 방식은 다음과 같다. 

단말에서는 다른 사람으로부터 SMS 문자를 받았을 때 텔레포니(Telephony) 모듈이 처리하게 한다. 이렇게 처리된 정보는 인텐트에 담겨 브로드캐스팅 방식으로 다른 앱에 전달되는 것이다. 직접 만든 브로드캐스트 수신자는 매니페스트 파일에 등록되었기 때문에 시스템이 이미 알고 있고, 따라서 자신이 직접 만든 앱에도 인텐트를 전달 받는 것이다. 여기서 인텐트를 받았을 때 onReceive() 메서드가 자동으로 호출된다. 

 

브로드캐스트 수신자를 사용하면서 주의할 점은 앱 A가 실행되고 있지 않아도 앱 A가 원하는 브로드캐스트 메시지가 도착하면 다른 앱 B를 실행하고 있는 도중에도 앱 A가 실행될 수 있다는 점이다. 이 때문에 동일한 SMS 수신 앱을 여러 개 수정하여 만들어 설치하면 오류가 발생했을 때 어느 앱에서 생긴 오류인지 찾아내기 힘든 경우가 많다. 그렇기 때문에 구 개발 버전의 앱을 한 번 설치한 후 앱의 패키지 이름을 수정하는 등의 방법으로 새 개발 버전의 앱을 만들었을 경우에는 구 개발 버전의 앱을 삭제하는 것이 좋다.

 

 

 

 

위험 권한 부여하기

위의 RECEIVE_SMS 권한은 위험 권한으로 분류된다. 마시멜로 버전부터는 권한을 일반 권한(Normal Permission)과 위험 권한(Dangerous Permission)으로 나누었는데 그 이유는 앱을 설치하는 시점에 사용자에게 물어보는 기존의 방식은 사용자가 아무런 생각 없이 앱을 설치하는 경우가 많았고, 이에 따라 설치된 앱들이 단말의 주요 기능을 마음대로 사용할 수 있었기 때문이다. 그래서 위험 권한으로 분류된 권한들에 대해서는 앱을 설치할 때가 아니라 앱을 실행할 때 권한을 부여하도록 만들었다. 

일반 권한과 위험 권한의 차이점

예를 들어, INTERNET 권한은 일반 권한이기 때문에 사용자가 앱을 설치할 때 권한을 부여할 것인지 물어본다. 하지만 위험 권한으로 분류된 RECEIVE_SMS 권한은 설치 시에 부여한 권한은 의미가 없으며 실행 시에 권한을 부여할 것인지 물어보게 된다. 만약 사용자가 권한을 부여하지 않으면 해당 기능은 동작하지 않는 것이다.

 

위험 권한으로 분류된 주요 권한들을 보면 대부분 개인정보가 담겨있는 정보에 접근하거나 개인정보를 만들어낼 수 있는 단말의 주요 장치에 접근할 때 부여된다. 대표적인 위험 권한은 위치, 카메라, 마이크, 연락처, 전화, 문자, 일정, 센서 정보가 될 수 있다. 또한 SD 카드에 접근할 때 사용하는 READ_EXTERNAL_STORAGE와 WRITE_EXTERNAL_STORAGE도 위험 권한으로 분류된다.

 

이제 위험 권한을 부여하는 방법을 알아보자. 과거에는 build.gradle(Module:app) 파일의 targetSdkVersion 값을 23 미만으로 설정하면 API 23 버전 이후의 플랫폼에서 검증된 앱이 아니라고 인식하여 위험 권한도 자동으로 부여가 되었는데, 현재는 이렇게 자동 부여하는 방식을 더 이상 사용할 수 없게 되었다. 위험 권한을 부여하는 방법은 직접 코드를 추가하는 방법과 외부 라이브러리를 이용한 위험 권한 자동 부여 방법이 있다. 

 

먼저, 위험 자동 부여 방법을 보면, 안드로이드 파이 버전에서는 AutoPermissions를 지원해 주기 때문에 다음 코드만 추가하면 쉽게 위험 권한 요청이 가능하다.

 

build.gradle(Module:app)

allprojects {
    repositories {
        maven { url 'https://jitpack.io' }
    }
}

dependencies {
    ...

    implementation 'com.github.pedroSG94:AutoPermissions:1.0.3'
}

 

MainActivity.java

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    AutoPermissions.Companion.loadAllPermissions(this, 101);
}

// 사용자 응답 결과
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    AutoPermissions.Companion.parsePermissions(this, requestCode, permissions, this);
}

// 사용자 응답이 denied 일 때 콜백
@Override
public void onDenied(int i, String[] strings) {
    Toast.makeText(this, "permissions denied : " + strings.length, Toast.LENGTH_LONG).show();
}

// 사용자 응답이 granted 일 때 콜백
@Override
public void onGranted(int i, String[] strings) {
    Toast.makeText(this, "permissions granted : " + strings.length, Toast.LENGTH_LONG).show();
}

이 코드는 위험 권한을 자동으로 부여하는 코드로 AndroidManifest.xml 파일 안에 넣은 권한 중에서 위험 권한을 자동으로 체크한 후 권한 부여를 요청하는 간편한 방식이다. onCreate() 메서드 안에서 loadAllPermissions() 메서드를 호출하면서 자동으로 권한을 부여하도록 요청하는 것이다. 

 

이제는 직접 추가하는 코드로 위험 권한을 요청해 보자. 그러기 위해서는 MainActivity.java에서 아래 코드를 추가해야 한다.

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    String[] permissions = {
            Manifest.permission.READ_EXTERNAL_STORAGE,
            Manifest.permission.WRITE_EXTERNAL_STORAGE
    };

    checkPermissions(permissions);
}

checkPermission() 메서드는 정해준 권한들에 대해서 그 권한이 부여되어 있는지를 먼저 확인한다. 그리고 권한이 부여되지 않았다면 ArrayList 안에 넣었다가 부여되지 않은 권한들만 권한 요청을 하게 된다.

private void checkPermissions(String[] permissions) {
    ArrayList<String> targetList = new ArrayList<>();

    for (int i=0; i< permissions.length; ++i) {
        String curPermission = permissions[i];
        int permissionCheck = ContextCompat.checkSelfPermission(this, curPermission);

        if (permissionCheck == PackageManager.PERMISSION_GRANTED)
            Toast.makeText(this, curPermission + " 권한 있음.", Toast.LENGTH_SHORT).show();
        else {
            Toast.makeText(this, curPermission + " 권한 없음.", Toast.LENGTH_SHORT).show();

            if (ActivityCompat.shouldShowRequestPermissionRationale(this, curPermission))
                Toast.makeText(this, curPermission + " 권한 설명 필요함.", Toast.LENGTH_SHORT).show();
            else
                targetList.add(curPermission);
        }
    }

    String[] targets = new String[targetList.size()];
    targetList.toArray(targets);

    ActivityCompat.requestPermissions(this, targets, 101);  // 위험 권한 부여 요청
}

checkSelfPermission() 메서드로 이미 권한이 부여되어 있는지 확인하도록 만들고, 권한이 부여되지 않았다면 requestPermission() 메서드를 호출하여 권한 부여 요청 대화상자를 띄워준다. 이 대화상자는 직접 만드는 것이 아니라 시스템이 띄워주기 때문에 사용자가 수락했는지 거부했는지는 콜백 메서드로 받아 확인해야 한다. 그 콜백 메서드가 onRequestPermissionResult()이다.

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    if (requestCode == 101) {
        if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED)
            Toast.makeText(this, "첫 번째 권한을 사용자가 승인함.", Toast.LENGTH_SHORT).show();
        else
            Toast.makeText(this, "첫 번째 권한 거부됨.", Toast.LENGTH_SHORT).show();
    }
}

이 메서드에는 요청 코드와 함께 사용자가 권한을 수락했는지 여부다 파라미터로 전달된다. 여러 권한을 한 번에 요청할 수도 있기 때문에 grantResults 배열 변수 안에 수락 여부를 넣어 전달한다. 이 예제에서는 SD카드에 접근하는 READ/WRITE 두 개의 권한을 요청했으므로 grantResults 배열의 길이는 2이며, grantResults[0]은 그 중 첫 번째 권한에 대해 수락되었는지 여부를 확인한 후 토스트 메시지를 띄우는 코드이다.

 

 

 

 

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
글 보관함