# In-App Billing을 구현해 보자.
- 아래는 전반적인 구현 순서이다.
1. in-app billing 샘플 애플리케이션을 다운 받는다.
2. IMarketBillingService.aidl 파일을 당신의 프로젝트에 추가한다.
3. Manifest.xml을 업데이트한다.
4. Service를 만들어 MarketBillingService 와 묶는다. 이는 구글 플레이 요청과 응답을 받기 위함이다.
5. broadcast intents와 관련된 응답을 처리하기 위한 BroadcastReceiver를 만든다.
6. in-app billing이 지원하는 애플리케이션 코드를 수정한다.
@ Downloading the Sample Application
- 샘플 애플리케이션의 구현 내용은 다음과 같다.
1. 구글 플레이에 결제 요청을 보낼 수 있다.
2. 동기, 비동기 응답을 처리 할 수 있다.
3. 보안 매커니즘을 통한 결제의 무결성을 식별할 수 있다.
4. 결제 아이템을 위한 UI를 제공한다.
# 샘플 애플리케이션 소스 설명과 다운 로드는 아래 이미지 참고.
- 만약 샘플 애플리케이션을 실제로 실행해서 결제를 일으키기 위해서는 아래와 같은 작업이 필요하다.
1. 애플리케이션을 구성한 후 빌드한다.
[ src/com/example/dungeons/Security.java 의
String base64EncodedPublicKey = "your public key here";
에 판매계정의 공개키를 넣는다. 패키지 네임을 바꾸고 빌드한다.]
2. 구글 플레이에 올린다.
[draft 상태(not publish)로 애플리케이션을 올리고 상품을 등록한다.]
3. 테스트 계정을 세팅하고 샘플 애플리케이션 실행시킨다.
[에뮬레이터에서 테스트가 불가능 하므로 다바이스에 설치해서 실행 시킨다. 이때 테스트 계정을 등록해서 디바이스의 primary 계정으로 넣어서 실행시킨다. 테스트 계정의 경우 환불이 가능하다.]
Consts.java 파일을 수정해서 디버깅 내용을 볼 수 있다.
@ Adding the AIDL file to your project
- com.android.vending.billing 패키지를 구현하고자 하는 사용자 애플리케이션에 넣는다.
@ Updating Your Application's Manifest
- 인앱 결제의 경우 구글 플레이 애플리케이션과 통신을 하기 때문에 이를 위한 권한이 Manifest에 설정 되어야 한다.
<uses-permission android:name="com.android.vending.BILLING" />
- 비동기 메시지(broadcast intent) 응답를 받기 위해 아래와 같이 BroadcastReceiver를 등록한다.이 때 인텐트 필터를 꼭 설정한다.
<receiver android:name="BillingReceiver">
<intent-filter>
<action android:name="com.android.vending.billing.IN_APP_NOTIFY" />
<action android:name="com.android.vending.billing.RESPONSE_CODE" />
<action android:name="com.android.vending.billing.PURCHASE_STATE_CHANGED" />
</intent-filter>
</receiver>
- IMarketBillingService 와 바인드해서 구글 플레이에 메시지를 보낼 수 있는 서비스를 등록 시킨다.
<service android:name="BillingService" />
# 샘플 애플리케이션에서 BillingReceiver는 BroadcastReceiver로 broadcast 메시지를 다루고, BillingService는 Service로 요청 메시지를 구글 플레이에 보낸다.
@ Creating a Local Service
- 로컬 Services는 구글 플레이와 메시지(동기) 교환을 위해 아래와 같이 구현을 해서 사용할 수 있다.
1. MarketBillingService를 바인드 시킨다. 서비스이 onCreate()에서 바인드를 시킨다.
try {
boolean bindResult = mContext.bindService(
new Intent("com.android.vending.billing.MarketBillingService.BIND"), this,
Context.BIND_AUTO_CREATE);
if (bindResult) {
Log.i(TAG, "Service bind successful.");
} else {
Log.e(TAG, "Could not bind to the MarketBillingService.");
}
} catch (SecurityException e) {
Log.e(TAG, "Security exception: " + e);
}
- 바인드 시킨 후에는 IMarketBillingService inferface로 billing 요청을 IPC 방식으로 보낼 수 있다. onServiceConnected()는 서비스 연결시 호출되는 콜백 메서드이다.
**
* The Android system calls this when we are connected to the MarketBillingService.
*/
public void onServiceConnected(ComponentName name, IBinder service) {
Log.i(TAG, "MarketBillingService connected.");
mService = IMarketBillingService.Stub.asInterface(service);
}
- 위의 메서드가 호출되면 개발자는 sendBillingRequest() 메서드를 호출할수 있다. 샘플 애플리케이션에서 서비스 바인드의 구현은 BillingService를 보면 된다.
2. 결제 요청 메시지를 보낸다.
- Service가 바인드 되면 IMarketBillingService의 sendBillingRequest()메서드(샘플 애플리케이션 기준)를 통해 통신을 한다. 이때 파라미터는 Bundle를 포함한다. 이 요청 메시지는 동기 응답을 주는데(결과 코드를 포함한) 결제 완료를 의미하진 않는다.
# 요청 메시지 타입
- CHECK_BILLING_SUPPORTED : 구글플레이가 결제를 지원하는지 확인 요청
- REQUEST_PURCHASE : 아이템 결제를 요청
- GET_PURCHASE_INFORMATION : 결제(환불) 내역을 요청
- CONFIRM_NOTIFICATIONS : 결제(환불) 내역 요청을 확인 알림
- RESTORE_TRANSACTIONS : 관리되는 결제(managed purchase, 한 유저에 한번 결제되는 아이템) 기록을 요청해서 확인함
- 관련 요청을 하기 위해서는 Bundle 파라미터를 만들어야 한다. 이때 BILLING_REQUEST, API_VERSION, PACKAGE_NAME 을 포함해야 한다.
protected Bundle makeRequestBundle(String method) {
Bundle request = new Bundle();
request.putString(BILLING_REQUEST, method);
request.putInt(API_VERSION, 1);
request.putString(PACKAGE_NAME, getPackageName());
return request;
# 인앱 결제 요청은 애플리케이션 메인 스레드에서 실행해야 한다.
* Verifying that in-app billing is supported (CHECK_BILLING_SUPPORTED)
/**
* Request type is CHECK_BILLING_SUPPORTED
*/
Bundle request = makeRequestBundle("CHECK_BILLING_SUPPORTED");
Bundle response = mService.sendBillingRequest(request);
// Do something with this response.
}
- 동기 응답(response) 번들의 RESPONSE_CODE의 값은 다음과 같다.
- RESULT_OK : 인앱 결제가 지원된다.
- RESULT_BILLING_UNAVAILABLE : 인앱 결제가 지원되지 않는다.API 버전 때문이거나 제한된 특정 국가의 유저이기 때문이다.
- RESULT_ERROR : 구글 플레이 애플리케이션의 접속 에러
- RESULT_DEVELOPER_ERROR : 퍼미션(com.android.vending.BILLING)을 선언하지 않거나 적절하지 않은 사인을 사용하거나 잘못된 요청을 보냈을 때.
- CHECK_BILLING_SUPPORTED 요청은 어떤 비동기 응답을 위한 요청이 되지 않는다. 즉, 동기 응답만 있음.
- CHECK_BILLING_SUPPORTED 요청시 RemoteException block 통해 예외 사항을 사용자에게 알려 주도록 한다. 주 예외 사항은 구글 플레이 애플리케이션이 업데이트가 필요한 경우이다. 이와같은 오류의 메시지는 샘플 애플리케이션의 DIALOG_CANNOT_CONNECT_ID in Dungeons.java에서 볼수 있다.
* Making a purchase request (REQUEST_PURCHASE)
1. REQUEST_PURCHASE 요청을 보낸다. (Making the request)
- 앞서 언급한 3가지 키-밸류는 모든 요청에 필수적으로 포함해야 한다.
[ BILLING_REQUEST, API_VERSION, PACKAGE_NAME ]
이외에 REQUEST_PURCHASE요청에는 ITEM_ID를 키로 하는 productid(필수)와 DEVELOPER_PAYLOAD를 키로 하는 developerPayload(선택)이 넣는다.
/**
* Request type is REQUEST_PURCHASE
*/
Bundle request = makeRequestBundle("REQUEST_PURCHASE");
request.putString(ITEM_ID, mProductId);
// Note that the developer payload is optional.
if (mDeveloperPayload != null) {
request.putString(DEVELOPER_PAYLOAD, mDeveloperPayload);
}
Bundle response = mService.sendBillingRequest(request);
// Do something with this response.
- 동기 응답 response 번들의 값은 다음과 같다.
- RESPONSE_CODE : 요청 상태 코드
- REQUEST_ID : 요청에 대한 식별자(중복 x)
- PURCHASE_INTENT : PendingIntent으로 checkout UI 실행시 사용할 수 있다.
2. 리턴 받은 PendingIntent를 실행시킨다. (Using the pending intent)
- PendingIntent를 실행 시키는 방법은 Android OS버전에 따라 다르다. 1.6의 경우 반드시 사용자 애플리케이션 액티비티에 스택하는 게 아니라 PendingIntent의 구별된 테스크에서 실행 해야 한다. 2.0이상의 경우 사용자 애플리케이션 액티비티에 스택 시켜서 실행 시킬 수 있다.
void startBuyPageActivity(PendingIntent pendingIntent, Intent intent) {
if (mStartIntentSender != null) {
// This is on Android 2.0 and beyond. The in-app checkout page activity
// will be on the activity stack of the application.
try {
// This implements the method call:
// mActivity.startIntentSender(pendingIntent.getIntentSender(),
// intent, 0, 0, 0);
mStartIntentSenderArgs[0] = pendingIntent.getIntentSender();
mStartIntentSenderArgs[1] = intent;
mStartIntentSenderArgs[2] = Integer.valueOf(0);
mStartIntentSenderArgs[3] = Integer.valueOf(0);
mStartIntentSenderArgs[4] = Integer.valueOf(0);
mStartIntentSender.invoke(mActivity, mStartIntentSenderArgs);
} catch (Exception e) {
Log.e(TAG, "error starting activity", e);
}
} else {
// This is on Android 1.6. The in-app checkout page activity will be on its
// own separate activity stack instead of on the activity stack of
// the application.
try {
pendingIntent.send(mActivity, 0 /* code */, intent);
} catch (CanceledException e) {
Log.e(TAG, "error starting activity", e);
}
}
}
# 개발자는 activity context(not an application context)에서 pending된 intent를 실행시켜야 한다. 또한 singleTop 모드로 실행 시킬 수 없다. 만약 singleTop모드로 실행시키면 intent는 애플리케이션 프로세스에 붙을 수 없다. 즉, 구글 플레이를 상단으로 가져오면 당신의 애플리케이션는 방해를 하게 될 것이다.
3. broadcast intents 응답을 처리한다. (Handling broadcast intents)
- 사용자 애플리케이션이 결제 요청(REQUEST_PURCHASE)를 하면 두개의 비동기 응답이 오게 된다.
1. 첫째는 RESPONSE_CODE를 포함한 broadcast이다. 이는 결제 요청에 대한 에러 정보를 가진다. 만약 RESULT_OK가 오면 이건 요청이 잘 전달되었다는 신호이다. (결제가 완료 되었다는 건 아니다.)
2. 둘째는 IN_APP_NOTIFY 브로드캐스트이다. 이는 결제가 체결되었는지를 알려주는 응답이다.
- 결제 과정은 바로 이루어 지지 않는다. 그래서 지연되고 완료되는 과정을 구글 플레이는 알려준다. 이 디스플레이는 60초가 지나면 사라진다. 사용자는 이 디스플레이에 의존할 필요없이 개별의 UI를 통해 진행상황을 알려 줄 수 있다.
# 개발자은 managed item 결제를 할때, 결제가 진행되는 동안 동일한 결제를 막을 필요가 있다. 구글 플레이에서는 사용자가 동일한 아이템을 두번 결제를 진행 시키면 에러를 사용자에게 표시 할것이다. 그리고 당신의 애플리케이션은 두번째 결제에 대한 IN_APP_NOTIFY 메시지를 계속 기다려, 문제가 발생하게 된다.
* Retrieving transaction information for a purchase or refund(GET_PURCHASE_INFORMATION)
- IN_APP_NOTIFY 응답은 결제 정보를 알 수 있는 notification id를 포함하고 있다. 결제 정보를 알기 위해 5가지 키를 가지는 번들을 만들어 요청을 보내야 한다. 코드는 아래와 같다.
/**
* Request type is GET_PURCHASE_INFORMATION
*/
Bundle request = makeRequestBundle("GET_PURCHASE_INFORMATION");
request.putLong(REQUEST_NONCE, mNonce);
request.putStringArray(NOTIFY_IDS, mNotifyIds);
Bundle response = mService.sendBillingRequest(request);
// Do something with this response.
}
- 구글 플레이에서 위와 같은 요청으로 RESPONSE_CODE와 REQUEST_ID를 키로하는 값을 가진 번들을 보낸다(동기 응답).
- GET_PURCHASE_INFORMATION 요청은 두가지 비동기 응답을 가진다.(비동기)
1. 구글 플레이에서 RESPONSE_CODE로 요청에 대한 정보를 보낸다. 요청이 제대로 전달되었는지를 알려준다.(결제완료 아님)
2. 둘째로 PURCHASE_STATE_CHANGED 응답을 보낸다. 이는 결제 정보를 가지고 있다. 결제 정보는 a signed JSON string(unencrypted)이다. 메시지는 결제의 무결성을 확인 할수있는 사인(signature)를 포함하고 있다.
* Acknowledging transation information (CONFIRM_NOTIFICATIONS)
- 결제 정보를 받았으면 반드시, 확인 메시지를 보내야 한다. 이때 네가지 키에 값을 넣은 번들을 보내야 한다.
/**
* Request type is CONFIRM_NOTIFICATIONS
*/
Bundle request = makeRequestBundle("CONFIRM_NOTIFICATIONS");
request.putStringArray(NOTIFY_IDS, mNotifyIds);
Bundle response = mService.sendBillingRequest(request);
// Do something with this response.
}
- 위의 요청으로 동기 응답을 받는다. 응답은 RESPONSE_CODE와 REQUEST_ID를 가진다.
- 또한 위의 요청으로 비동기 응답(RESPONSE_CODE broadcast intent)을 가져온다. 이는 요청의 상태와 에러 정보를 알려준다.
- 확인 요청은 IN_APP_NOTIFY 응답을 받은 후 반드시 보내야 한다. 그렇지 않으면 구글 플레이는 계속적으로 IN_APP_NOTIFY 응답을 보낸다. 또한 다중 주문에 대해서도 처리해야 하기 때문에 확인 응답을 보내야 한다.
# 가장 좋은 예는 사용자가 결제를 통해 관련 서비스를 이용가능하면 확인 메시지를 보내는 것이다.
* Restoring transaction information (RESTORE_TRANSACTIONS)
- 결제 내역을 회복시키 위해서는 4가지 키가 필요하다. 기본 키 3가지와 REQUEST_NONCE키에 암호화 된 nonce값을 필요로 한다. 이 nonce는 PURCHASE_STATE_CHANGED broadcast intent 응답에서 결제를 식별하기 위해 제공한다.
/**
* Request type is RESTORE_TRANSACTIONS
*/
Bundle request = makeRequestBundle("RESTORE_TRANSACTIONS");
request.putLong(REQUEST_NONCE, mNonce);
Bundle response = mService.sendBillingRequest(request);
// Do something with this response.
}
- 위의 요청으로 동기 응답을 받는다. RESPONSE_CODE와 REQUEST_ID를 키로 하는 번들이다. RESPONSE_CODE는 요청의 전송에 관한 상태 응답, REQUEST_ID 요청에 대한 식별자 이다.
- 위의 요청으로 또한 두가지 비동기 응답을 받을 수 있다.
1. 첫째로 RESPONSE_CODE broadcast intent로서 요청에 대한 상태 정보를 보내 준다.
2. 둘째로 요청이 성공하면 요청에 대한 정보를 PURCHASE_STATE_CHANGED broadcast intent로 받는다. 이 메시지는 세부적인 결제 정보를 가지고 있다.이 정보는 JSON string(암호화 되지 않음) 타입이다.
# RESTORE_TRANSACTIONS요청은 애플리케이션이 처음 디바이스에서 실행될때 그리고 삭제되고 다시 설치되었을때 보내야한다.