О чем это вообще?
С версией приложения Android Market 2.3.0 для разработчиков приложений для платформы Android открылась возможность предоставлять пользователям платежи внутри самих приложений. Теперь можно продавать уровни и артефакты, видео, музыку, плагины и прочее, пользуясь лишь встроенными средствами платформы. Давайте увидим, как это можно сделать.
Что нам понадобится?
Как обычно, любимая IDE, Android SDK и пример приложения.
Так же будет полезным представлять себе, что такое Service, BroadcastReceiver и, конечно, Activity.
Так же нам понадобится разрешение в файле манифеста —
<uses-permission android:name="com.android.vending.BILLING"/>
, без него ничего не заработает.Как это в принципе работает?
Работает все через сервис в приложении Android Market. Он умеет посылать запросы на получение деталей определенной вещи, которую хочет купить пользователь, на покупку, получать ответы о успехе или неудачи покупки, и прочее. Вся информация о вещах, которые вы продаете, должна быть заведена через Консоль Разработчика для конкретного приложения. Как только мы получили сигнал об успешной покупке, мы можем начать грузить с нашего сервера контент.
Как все это устроено?
![image](https://habrastorage.org/getpro/habr/post_images/c82/52f/400/c8252f4003940da3c46474c0ff1671ab.png)
На сервере в маркете хранится информация о вещах, которые можно купить. С сервером взаимодействует клиентское приложение маркета. С ним взаимодействует наше приложение.
Наше приложение будет состоять как минимум из:
- BillingService. Это сервис, который связан с приложением маркета, и отправляет ему всю информацию об операциях и получает на них ответы, в случае, если они синхронные.
- BillingReceiver. Получает асинхронные ответы от приложения маркета.
- PurchaseObserver. Сущность, которая будет оповещать UI об изменениях состояния покупок.
Какие сообщения мы отсылаем маркету?
Вначале нам нужно соединить наш BillingService с приложением маркета для того, чтобы вызывать метод sendBillingRequest сервиса MarketBillingService. Интерфейс сервиса описан в файле IMarketBillingService.aidl, его можно скачать
Сам метод принимает параметр Bundle, в котором и хранится вся информация. Самой важной является параметр с ключом «BILLING_REQUEST». Он определяет тип запроса. Они бывают:
•
CHECK_BILLING_SUPPORTED
– проверка доступности in-app billing.•
REQUEST_PURCHASE
– запрос покупки.•
GET_PURCHASE_INFORMATION
– получение информации об изменении состояния покупки.•
CONFIRM_NOTIFICATIONS
– подтверждение факта получение уведомления от приложения маркета.•
RESTORE_TRANSACTIONS
– восстановление транзакций по уже купленным вещам.Какие получаем ответы?
Метод синхронно возвращает ответ, который тоже представляет из себя Bundle. В нем лежит:
•
RESPONSE_CODE
— код ответа•
PURCHASE_INTENT
— PendingIntent
, для того, чтобы запустить активти покупки.•
REQUEST_ID
– идентификатор посланного запросаВ случае асинхронных ответов(получаемых через BillingReceiver), в них лежит следующее:
•
com.android.vending.billing.RESPONSE_CODE
Код ответа. Маркет подтверждает успех или неудачу посылки запроса.
•
com.android.vending.billing.IN_APP_NOTIFY
Оповещение о том, что статус покупки изменился. После этого сообщения нужно отправлять запрос с типом
GET_PURCHASE_INFORMATION
, чтобы получить инфу.•
com.android.vending.billing.PURCHASE_STATE_CHANGED
А тут приходит детальная информация о покупке. Она включает в себя nonce, список заказов, с указанием идентификаторов продуктов, их состояний и проч.
Последовательность при покупке будет такая:
1. Посылаем
REQUEST_PURCHASE
2. Получаем синхронный ответ
3. Запускаем Activity покупки(также встроенную в приложение маркета)
4. Получаем асинхронное сообщение
IN_APP_NOTIFY
5. Посылаем
GET_PURCHASE_INFORMATION
6. Получаем синхронный ответ
7. Получаем асинхронный ответ
PURCHASE_STATE_CHANGED
8. Отправляем
CONFIRM_NOTIFICATIONS
9. Получаем синхронный ответ
А может, посмотрим лучше на код?
Итак, основные моменты в коде:
1. Коннект к маркету.
В onCreate() в BillingService пишем:
try {
boolean bindResult = getApplicationContext().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);
}
//И чуть ниже:
@Override
public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
Log.i(TAG, "MarketBillingService connected.");
marketService = IMarketBillingService.Stub.asInterface(iBinder);
runPendingRequests();
}
* This source code was highlighted with Source Code Highlighter.
2. Отправка запросов.
В примере приложения создана иерархия классов для запросов и это правильно. Самая главная вещь в них – это отложенная отправка. Дело в том, что bindService происходит асинхронно, а значит ссылку на MarketBillingService мы получаем гораздо позже, чем кончается onCreate(), и даже позже, чем пытаемся в первый раз выполнить запросы. Поэтому делаем так:
/**
* Run the request, starting the connection if necessary.
*
* @return this request if the request was not executed in order to queue it.
*/
public final AbstractRequest runRequest() {
if (runIfConnected()) {
return null;
}
return this;
}
/**
* Try running the request directly if the service is already connected.
*
* @return true if the request ran successfully; false if the service
* is not connected or there was an error when trying to use it
*/
public boolean runIfConnected() {
if (service.isConnected()) {
try {
requestId = run();
if (requestId >= 0) {
service.addRequest(requestId, this);
}
return true;
} catch (MyException e) {
onException(e);
}
}
return false;
}
* This source code was highlighted with Source Code Highlighter.
Возвращение запроса нужно, чтобы запомнить его в списке ожидающих отправки запросов в сервисе. Потом, когда мы получим ссылку на MarketBillingService в onServiceConnected(), мы все запросы попробуем отправить еще раз.
3. Оповещение UI
В BillingService будем хранить ссылку на некую сущность, которая хранит в себе Handler от нашего UI. Тогда по получении ответов можно будет делать следующее:
private void notifyGui(int messageId, PurchasedItem item) {
if (observer != null) {
observer.notifyGui(messageId, item);
} else {
// пошлем здесь оповещение в NotificationBar
}
}
* This source code was highlighted with Source Code Highlighter.
Важно: не забывать обнулять observer сервиса из своей Activity при выходе из нее и восстанавливать эту ссылку. Делается это так:
@Override
protected void onUserLeaveHint() {
if (billingService != null) {
unbindService(this);
billingService.resetEventDispatcher();
billingService = null;
}
super.onUserLeaveHint();
}
@Override
public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
//Connected to BillingService
if (componentName.getShortClassName().equals(serviceClassName)) {
billingService = ((BillingService.LocalBinder) iBinder).getService();
billingService.setObserver(new PurchaseObserver());
try {
billingService.checkBillingAvailability();
} catch (MyException e) {
Log.e(TAG, "Billing unavailable", e);
Toast.makeText(this, "Billing unavailable", Toast.LENGTH_LONG).show();
}
}
}
* This source code was highlighted with Source Code Highlighter.
4. Запуск Activity покупки.
Загляните в PurchaseObserver класс примера. Там есть метод public void startBuyPageActivity(PendingIntent pendingIntent, Intent intent). PendingIntent – это то, что мы получили ответом от маркета. А Intent – просто new Intent().
Что же этот метод делает?
На самом деле в этот класс передается инстанс вашей Activity. Дальше от нее через reflection происходит попытка получить метод startInstentSender. Этот метод появился только в Android 2.0. Если он есть, то метод вызывается, и Activity запускается в стеке Activity нашего приложения. Если же метод не найден, то происходит старт Activity в отдельном стеке.
А как же безопасность?
Вопрос безопасности – тема отдельной статьи, и так уже много. В примере приложения за безопасность отвечает класс Security. Скажу только, что верифицировать покупки нужно не в приложении(как это сделано в примере), а на собственном сервере, дабы не давать логику проверки в руки потенциальных обладателей apk.
P.S. На основе developer.android.com/guide/market/billing/billing_overview.html