Пролог
Эта статья является продолжением предыдущей статьи, которая была написана в формате сделай за 5 минут. Эта статья является первой ступенькой для того, чтоб разобраться в вопросе, так как информации в русском сегменте интернета об IABv3 почти нет, а что есть, то передается из уст в уста.
Речь пойдет о последней библиотеке для приема платежей In-app billing version 3 от GooglePlay, на которую начался переход с начала декабря 2012 года.
Теория
Магазин GooglePlay позволяет нам создавать и продавать 3 типа продуктов:
- Контролируется Google — продукт, который может быть куплен пользователем единожды;
- Не контролируется Google — продукт, при расходовании которого, можно купить многократно;
- Подписка — продукт, на который пользователь подписывается и с него снимается плата каждые N-дней.
Библиотека IABv3 считает все эти продукты Managed (управляемые), то есть при запросе у магазина нашим приложением списка купленных продуктов, мы получим список всех купленных наименований, но при этом товары типа Не контролируется Google могут быть израсходованы, при этом появится возможность повторной их покупки.
Практика
И так, в прошлой статье мы рассмотрели как установить примеры использования биллинга от Google.
Откроем пример TrivialDrive, который расположен «Путь к расположению нашей SDKextrasgoogleplay_billingin-app-billing-v03samplesTrivialDrive» и рассмотрим его более подробно.
Внутри мы видим три пакета:
- com.android.vending.billing в котором находится файл IInAppBillingService.aidl являющийся описанием интерфейса взаимодействия с сервисом биллинга в приложении «Play Маркет»;
- com.example.android.trivialdrivesample в котором находится сам пример приложения;
- com.example.android.trivialdrivesample.util в котором находятся скрипты хелпера, что упрощает нам жизнь, при внедрении функции платежей в наших приложениях.
Для того, чтоб приложение могло взаимодействовать с биллингом магазина в AndroidManifest.xml добавляется следующее разрешение:
<uses-permission android:name="com.android.vending.BILLING" />
Это разрешение позволяет нам подключаться к Сервису биллинга приложения «Play Маркет».
Далее мы подробно разберем что происходит в самом приложении MainActivity.java, что в com.example.android.trivialdrivesample.
Определяются следующие переменные и константы для работы приложения:
// Метка для ведения логов
static final String TAG = "TrivialDrive";
// Переменная хранящая состояние активности премиум опции
boolean mIsPremium = false;
// Индефикаторы двух продуктов: premium (контролируется Google) и gas (не онтролируется Google)
static final String SKU_PREMIUM = "premium";
static final String SKU_GAS = "gas";
// (произвольный) код для возвращения в приложение данных при покупке из приложения "Play Маркет"
static final int RC_REQUEST = 10001;
// Массив графики для отображения состояния наполнения бака танка
static int[] TANK_RES_IDS = { R.drawable.gas0, R.drawable.gas1, R.drawable.gas2,
R.drawable.gas3, R.drawable.gas4 };
// Константа определяющая сколько SKU_GAS заполняет полностью бак танка.
static final int TANK_MAX = 4;
// Текущая наполненность бака танка
int mTank;
// Объект Хелпера для взаимодействия с биллингом.
IabHelper mHelper;
Переходим к методу onCreate:
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Читаем сохраненные настройки приложения
loadData();
// В переменной base64EncodedPublicKey должен быть указан открытый ключ RSA приложения вместо
// CONSTRUCT_YOUR_KEY_AND_PLACE_IT_HERE
String base64EncodedPublicKey = "CONSTRUCT_YOUR_KEY_AND_PLACE_IT_HERE";
// Защита от дурака, проверяет, действительно ли был указан ключ RSA
// и было ли переименовано приложение из стандартного
if (base64EncodedPublicKey.contains("CONSTRUCT_YOUR")) {
throw new RuntimeException("Please put your app's public key in MainActivity.java. See README.");
}
if (getPackageName().startsWith("com.example")) {
throw new RuntimeException("Please change the sample's package name! See README.");
}
// Создается новая копия объекта хелпера для взаимодействия с биллингом
Log.d(TAG, "Creating IAB helper.");
mHelper = new IabHelper(this, base64EncodedPublicKey);
// Включается режим логирования в хелпере
mHelper.enableDebugLogging(true);
// Начинается настройка хелпера. Это ассинхронная процедура со своими слушателями,
// которые будут вызванны по завешении получения данных.
Log.d(TAG, "Starting setup.");
mHelper.startSetup(new IabHelper.OnIabSetupFinishedListener() {
public void onIabSetupFinished(IabResult result) {
Log.d(TAG, "Setup finished.");
if (!result.isSuccess()) {
// Произошла ошибка, сообщаем о ней пользователю и выходим из метода
complain("Problem setting up in-app billing: " + result);
return;
}
// Хелпер полностью инициализирован, получаем
Log.d(TAG, "Setup successful. Querying inventory.");
mHelper.queryInventoryAsync(mGotInventoryListener);
}
});
}
На строке «mHelper.queryInventoryAsync(mGotInventoryListener);» начинается самое интересное, после инициализации Хелпера мы в асинхронном режиме получаем от магазина список купленных пользователем продуктов. mGotInventoryListener является методом обратного вызова, по завершении получения данных от магазина. Посмотрим же, что происходит в нем:
// Вызывается по завершении выполнения запроса купленных продуктов в магазине.
// Создаем новый экземпляр класса IabHelper.QueryInventoryFinishedListener и определяем в нем
// метод onQueryInventoryFinished
IabHelper.QueryInventoryFinishedListener mGotInventoryListener = new IabHelper.QueryInventoryFinishedListener() {
public void onQueryInventoryFinished(IabResult result, Inventory inventory) {
Log.d(TAG, "Query inventory finished.");
if (result.isFailure()) {
// Сообщаем пользователю о том, что не удалось получить список купленных продуктов в магазине
complain("Failed to query inventory: " + result);
// Выходим из метода
return;
}
// Удачно получили список купленных товаров в магазине, начинаем разбор
Log.d(TAG, "Query inventory was successful.");
// Проверяем есть ли у пользователя активированный режим премиум
mIsPremium = inventory.hasPurchase(SKU_PREMIUM);
Log.d(TAG, "User is " + (mIsPremium ? "PREMIUM" : "NOT PREMIUM"));
// проверяем, есть ли купленное топливо, если есть, то сразу заправляем его в бак
// (расходуем, чтоб пользователь мог еще раз этот товар)
if (inventory.hasPurchase(SKU_GAS)) {
Log.d(TAG, "We have gas. Consuming it.");
// Сообщаем магазину в асинхронном режиме, что товар израсходован.
mHelper.consumeAsync(inventory.getPurchase(SKU_GAS), mConsumeFinishedListener);
// выходим из метода
return;
}
// Если нет товаров, которые необходимо израсходовать, то обновляем интефейс программы.
updateUi();
// Отключаем заставку ожидания данных в программе.
setWaitScreen(false);
Log.d(TAG, "Initial inventory query finished; enabling main UI.");
}
};
И так, тут мы столкнулись с двумя типами продуктов:
- SKU_PREMIUM который имеет тип Контролируется Google
- SKU_GAS который имеет тип Не контролируется Google
Если при наличии первого мы просто делаем отметку в boolean переменной mIsPremium, то при наличии второго, мы запускаем метод расходования продукта «mHelper.consumeAsync(inventory.getPurchase(SKU_GAS), mConsumeFinishedListener);», чтоб пользователь мог купить его ещё раз. mConsumeFinishedListener является методом обратного вызова, который вызывается, когда магазин подтверждает расходование продукта. Посмотрим же, что происходит в нем:
// Вызывается при завершении процедуры подтверждения расходования продукта магазином
// Создаем новый экземпляр класса IabHelper.OnConsumeFinishedListener и определяем в нем
// метод onConsumeFinished
IabHelper.OnConsumeFinishedListener mConsumeFinishedListener = new IabHelper.OnConsumeFinishedListener() {
public void onConsumeFinished(Purchase purchase, IabResult result) {
Log.d(TAG, "Consumption finished. Purchase: " + purchase + ", result: " + result);
// Мы знаем, что "gas" это единственный продукт потребляемый в нашем приложении,
// но если в вашем приложении более одного потребляемого продукта,
// то имеет смысл тут установить проверку на идентификатор продукта.
if (result.isSuccess()) {
// Потребление продукта прошло успешно.
// Активируем логику приложения и заправим наш танк.
Log.d(TAG, "Consumption successful. Provisioning.");
mTank = mTank == TANK_MAX ? TANK_MAX : mTank + 1;
// Сохраняем данные о танке.
saveData();
// Сообщаем пользователю, на сколько заполнен бак его танка
alert("You filled 1/4 tank. Your tank is now " + String.valueOf(mTank) + "/4 full!");
} else {
// Иначе сообщаем пользователю об ошибке
complain("Error while consuming: " + result);
}
// Обновляем графический интерфейс приложения
updateUi();
// Отключаем заставку ожидания данных в программе.
setWaitScreen(false);
Log.d(TAG, "End consumption flow.");
}
};
Теперь разберем два метода, которые обрабатывают нажатие на кнопки в приложении.
Метод onBuyGasButtonClicked:
// Пользователь нажимает на кнопку "Buy Gas"
public void onBuyGasButtonClicked(View arg0) {
Log.d(TAG, "Buy gas button clicked.");
// Проверяем не поный ли бак у нашего танка
if (mTank >= TANK_MAX) {
// если бак полный, то сообщаем об этом пользователю
complain("Your tank is full. Drive around a bit!");
// и выходим из метода
return;
}
// Если же в бак можно еще залить бензина, то включаем заставку получения данных приложением
setWaitScreen(true);
Log.d(TAG, "Launching purchase flow for gas.");
// И через хелпер связываемся с магазином для покупки продукта
mHelper.launchPurchaseFlow(this, SKU_GAS, RC_REQUEST, mPurchaseFinishedListener);
}
Метод onUpgradeAppButtonClicked:
// Пользователь нажимает на кнопку "Upgrade to Premium".
public void onUpgradeAppButtonClicked(View arg0) {
Log.d(TAG, "Upgrade button clicked; launching purchase flow for upgrade.");
// Включаем заставку получения данных приложением
setWaitScreen(true);
// И через хелпер связываемся с магазином для покупки продукта
mHelper.launchPurchaseFlow(this, SKU_PREMIUM, RC_REQUEST, mPurchaseFinishedListener);
}
* Тут многие могут заметить, что в методе нет проверки переменной mIsPremium на её состояние, но могу успокоить, автор кода эту проверку осуществляет в методе updateUi().
Оба метода привязываются к кнопка непосредственно в xml описании активити по средствам указания у элементов кнопок параметра android:onClick.
android:onClick="onUpgradeAppButtonClicked"
Так же многие могли заметить, что при вызове метода покупки магазина они используют метод обратного вызова mPurchaseFinishedListener, посмотри, что происходит в нем:
// Вызывается при завершении процедуры покупки продукта в магазине
// Создаем новый экземпляр класса IabHelper.OnIabPurchaseFinishedListener и определяем в нем
// метод onIabPurchaseFinished
IabHelper.OnIabPurchaseFinishedListener mPurchaseFinishedListener = new IabHelper.OnIabPurchaseFinishedListener() {
public void onIabPurchaseFinished(IabResult result, Purchase purchase) {
Log.d(TAG, "Purchase finished: " + result + ", purchase: " + purchase);
if (result.isFailure()) {
// Произошла ошибка, сообщаем об этом пользователю
complain("Error purchasing: " + result);
setWaitScreen(false);
// Выходим из метода
return;
}
Log.d(TAG, "Purchase successful.");
// Проверяем, что же именно купил пользователь
if (purchase.getSku().equals(SKU_GAS)) {
Log.d(TAG, "Purchase is gas. Starting gas consumption.");
// Ели это оказалось топливо, то запускаем метод потребления продукта
mHelper.consumeAsync(purchase, mConsumeFinishedListener);
}
else if (purchase.getSku().equals(SKU_PREMIUM)) {
Log.d(TAG, "Purchase is premium upgrade. Congratulating user.");
// Если это оказалась опция премиум, то благодарим пользователя.
alert("Thank you for upgrading to premium!");
// И делаем соответствующие настройки в приложении
mIsPremium = true;
updateUi();
setWaitScreen(false);
}
}
};
Мы рассмотрели основные методы приложения, которые отвечают за работу с покупками внутри приложения, но остались ещё два метода, которые обязательно нужно описать, это onActivityResult и onDestroy.
Метод onActivityResult:
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
Log.d(TAG, "onActivityResult(" + requestCode + "," + resultCode + "," + data);
// Отдаем переменные в хелпер
if (!mHelper.handleActivityResult(requestCode, resultCode, data)) {
// если переменные не для хелпера, то передаем их дальше
super.onActivityResult(requestCode, resultCode, data);
}
else {
Log.d(TAG, "onActivityResult handled by IABUtil.");
}
}
Этот метод позволяет приложению «Play Маркета» передавать в ваше приложение данные о работе с продуктами. То есть возвращать результат ваших запросов к магазину.
Метод onDestroy:
@Override
public void onDestroy() {
Log.d(TAG, "Destroying helper.");
if (mHelper != null) mHelper.dispose();
mHelper = null;
}
В этом методе мы разрушаем связь Хелпера с сервисом приложения «Play Маркет».
Остальные методы приложения являются реализацией логики самой мини игры и они настолько просты, что не имеет смысла их рассматривать в рамках этой статьи.
Эпилог
В этой статье, мы разобрали по косточкам действующий пример Google по работе с платежами внутри программы, как применить эти знания в своем приложении, можно ознакомиться в предыдущей статье.
Автор: neoksi