In-app purchasing или внутренние платежи в приложениях для Android

О чем это вообще?



С версией приложения Android Market 2.3.0 для разработчиков приложений для платформы Android открылась возможность предоставлять пользователям платежи внутри самих приложений. Теперь можно продавать уровни и артефакты, видео, музыку, плагины и прочее, пользуясь лишь встроенными средствами платформы. Давайте увидим, как это можно сделать.

Что нам понадобится?



Как обычно, любимая IDE, Android SDK и пример приложения.
Так же будет полезным представлять себе, что такое Service, BroadcastReceiver и, конечно, Activity.

Так же нам понадобится разрешение в файле манифеста —

<uses-permission android:name="com.android.vending.BILLING"/>, без него ничего не заработает.

Как это в принципе работает?




Работает все через сервис в приложении Android Market. Он умеет посылать запросы на получение деталей определенной вещи, которую хочет купить пользователь, на покупку, получать ответы о успехе или неудачи покупки, и прочее. Вся информация о вещах, которые вы продаете, должна быть заведена через Консоль Разработчика для конкретного приложения. Как только мы получили сигнал об успешной покупке, мы можем начать грузить с нашего сервера контент.

Как все это устроено?



image

На сервере в маркете хранится информация о вещах, которые можно купить. С сервером взаимодействует клиентское приложение маркета. С ним взаимодействует наше приложение.

Наше приложение будет состоять как минимум из:
  1. BillingService. Это сервис, который связан с приложением маркета, и отправляет ему всю информацию об операциях и получает на них ответы, в случае, если они синхронные.
  2. BillingReceiver. Получает асинхронные ответы от приложения маркета.
  3. PurchaseObserver. Сущность, которая будет оповещать UI об изменениях состояния покупок.


Какие сообщения мы отсылаем маркету?



Вначале нам нужно соединить наш BillingService с приложением маркета для того, чтобы вызывать метод sendBillingRequest сервиса MarketBillingService. Интерфейс сервиса описан в файле IMarketBillingService.aidl, его можно скачать imageотсюда, а положить надо в com.android.vending.billing пакет вашего приложения. IDE должна сразу же сгенерить IMarketBillingService.java файл, который нам понадобиться для вызова вышеупомянутого метода.

Сам метод принимает параметр 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

Similar posts

AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 22

    +1
    Меня вот интересует, когда Гугл откроет возможность покупки для остальных стран? Только 14 стран могут покупать (http://ru.wikipedia.org/wiki/Android_Market) остальные курят. Как курят и разработчики, не имеющие возможность продавать. Как мне кажется гуглу бы расширить возмоности сначала горизонтально, а не вертикально.

    Вообще интересно, в чём причина такого ограничения? Сам живу в Эстонии. Мы как бы и Евросоюз, и валюта у нас как бы евро. А покупать ПО на маркете официально никак.
      +1
      это связано с аналитикой, google анализирует в каких странах платежеспособность нормальная.
        0
        Ну а что мешает открыть в странах где платёжеспособность меньше? По моему разумению это только подстёгивает пиратство программ в этих регионах. Мне кажется это не совсем та причина.

        en.wikipedia.org/wiki/Android_Market — уверен, платежеспособность населения Эстонии побольше Индонезии будет.
          0
          Даже Пакистан может покупать. А половина Европы не может. Непонятно.
            0
            На счёт market-enabler у вас Эстонии как? ;) Запрет вроде как-то с операторами связан, но это обходится элементарно подсовывая маркету кривой ID оператора. Гугл походу не против.
              0
              Я говорил про официальные способы. ;)
                0
                Ну он такой полу-официальный, в плане от него никому не плохо.
                  0
                  Ну это надо рут получать и иметь соответствующие знания. А как быть обычному пользователю?
                    0
                    Рут на большинстве девайсов получается в один клик вроде как. Но с некоторыми девайсами не везёт да.
                      0
                      Я повторюсь:
                      а) я говорю про обычных пользователей, не шибко дружных с техникой;
                      б) я говорю про легитимные способы покупок, ибо с рутом ты теряешь гарантию (про возврат к первоначальному состоянию знаю, но всё же)
                        0
                        Платежи от гугла — далеко не единственные. В наших проектах неплохо себя зарекомендовала библиотека от Fortumo, также пробовали аналоги от Zong и Boku.

                        Гугловский вариант пока отпадает из-за завязки к кредиткам, которые мало у кого вбиты, и ужасным покрытием по странам.
        0
        Инфа устарела, 29 стран могут продавать, а покупать — еще больше
        0
        Спасибо за статью
        • UFO just landed and posted this here
            +1
            кроме этого, гугл еще берет 2.9 % от платежей + 0.30$ каждый месяц.
            +1
            Довольно сложное изложение материала, так сказать «сразу к делу».
            Нехватает более общей инфы:
            Итемсы на продажу — это просто уникальные ID-шники ваших предметов(разделов) вашей проги, но не в коем случае еще какие-то apk.
            Вот как это выгляит:
            image
            в данном случае Potion и Sword это и есть те самые ID, в случае приобретения которых вы разлочите к ним доступ в основной проге.
            Следует учесть пару особенностей:
            • выбранные ID (100 символов) должны быть уникальны для всех ваших предыдущих и последующих программ
            • продаваемые итемсы(ID-шники) не могут повторно использоваться в других ваших программах

            На мой взгляд главная фишка In-app Billing, это то, что теперь можно делать бесплатную программу(игру) с ограниченным функционалом(меньшим количеством уровней, предметов), и в последствии предоставлять юзерам возможность докупать функционал, то есть, теперь не обязательно делать версии Free и Pro для маркета.
              0
              Очень яркий пример In-app Billing это симпотная игра Cordy, где главному персу игры можно за деньги менять цвет, докупать всякие «плюшки» в одежду и разблокировать доп уровни.
                0
                Potion и Sword — это только название айтемов(Title), их ID можно увидеть, кликнув по названию. Id — это In-app Product ID.
                  0
                  Верно, Potion и Sword это Titles, ID там наверняка другие
                +2
                Как разработчик я за это, как пользователь — против. Если раньше, покупая игру, вы покупали 100% функционала, то теперь даже полная версия приложения ничего не гарантирует.
                  0
                  Зато оно позволяет не продавать игру, а выпускать ее бесплатно и без рекламы, но с неполным количеством уровней/возможностей.
                  0
                  А возможно ли использовать эту систему для пополнения игрового счета прямо из приложения? Притом, что данные по счету хранятся на нашем сервере. То есть, нужна возможность проверять статус платежа именно с сервера. Плюс еще нужно как-то обрабатывать возможность отмены платежа пользователем. Помогите, пожалуйста, информацией по этому вопросу.

                  Only users with full accounts can post comments. Log in, please.