воскресенье, 25 ноября 2012 г.

Блоха из Мармелада (пара слов об отладке)

Продолжая тему разработки Marmalade-приложений, хотелось бы упомянуть об одном интересном приеме, облегчающем отладку приложений, совершающих внутренние покупки в App Store. Собственно выполнения самой покупки должно быть обязательно протестировано на IOS-устройстве, но приложение может содержать довольно сложную логику, связанную с выполнением покупки, которую, возможно, хотелось бы протестировать под отладчиком.

В качестве примера, рассмотрим приложение s3e/s3eIOSAppStoreBilling, поставляемое в составе examples к Marmalade. Если его запустить, то все что нам удастся увидеть — это модальное окно, с сообщением об ошибке «This example only works on iPhone with a test store set up for in-app purchase». Это конечно правильно, но нам то хочется отладить приложение до того, как мы его разместим на IPhone. Сделать это совсем не сложно. Мы разработаем заглушку, имитирующую (в отладочной сборке) поведение S3E iOS App Store Billing. Начнем с добавления наших файлов в mkb-файл:

#!/usr/bin/env mkb
files
{
    s3eIOSAppStoreBilling.cpp
    AppStoreStub.h
    AppStoreStub.cpp
}
...

Сам код заглушки примитивен. В h-файле для каждой функции App Store Billing мы доопределяем заглушку с суффиксом «Stub» и в случае если сборка отладочная, подменяем вызовы директивой #define:

#ifndef _APPSTORESTUB_H_
#define _APPSTORESTUB_H_

#include <string.h>
#include "s3e.h"#include "s3eIOSAppStoreBilling.h"
 
#define APP_STORE_TIMEOUT     1000
 
#if defined IW_DEBUG
#define s3eIOSAppStoreBillingAvailable   \
               s3eIOSAppStoreBillingAvailableStub
#define s3eIOSAppStoreBillingGetInt  \
               s3eIOSAppStoreBillingGetIntStub
#define s3eIOSAppStoreBillingInit \
               s3eIOSAppStoreBillingInitStub
#define s3eIOSAppStoreBillingStart \
               s3eIOSAppStoreBillingInitStub
#define s3eIOSAppStoreBillingTerminate \
               s3eIOSAppStoreBillingTerminateStub
#define s3eIOSAppStoreBillingStop \
               s3eIOSAppStoreBillingTerminateStub
#define s3eIOSAppStoreBillingRequestProductInformation  \
               s3eIOSAppStoreBillingRequestProductInformationStub
#define s3eIOSAppStoreBillingCancelProductInformationRequests \
               s3eIOSAppStoreBillingCancelProductInformationRequestsStub
#define s3eIOSAppStoreBillingRequestPayment \
               s3eIOSAppStoreBillingRequestPaymentStub
#define s3eIOSAppStoreBillingCompleteTransaction \
               s3eIOSAppStoreBillingCompleteTransactionStub
#define s3eIOSAppStoreBillingRestoreCompletedTransactions \
               s3eIOSAppStoreBillingRestoreCompletedTransactionsStub
#endif

s3eBool   s3eIOSAppStoreBillingAvailableStub();
int32     s3eIOSAppStoreBillingGetIntStub(s3eIOSAppStoreBillingProperty property);
s3eResult s3eIOSAppStoreBillingInitStub(s3eProductInformationCallbackFn infoCallback,  
                                        s3ePaymentTransactionUpdateCallbackFn updateCallback,  
                                        void* userData);
void      s3eIOSAppStoreBillingTerminateStub();
s3eResult s3eIOSAppStoreBillingRequestProductInformationStub(const char** productIdentifiers,  
                                        uint32 numProductIdentifiers);
void      s3eIOSAppStoreBillingCancelProductInformationRequestsStub();
s3eResult s3eIOSAppStoreBillingRequestPaymentStub(s3ePaymentRequest* paymentRequest);
s3eResult s3eIOSAppStoreBillingCompleteTransactionStub(s3ePaymentTransaction*  transaction,  
                                        s3eBool finalise);
s3eResult s3eIOSAppStoreBillingRestoreCompletedTransactionsStub();
void      appStoreStubUpdate();
#endif // _APPSTORESTUB_H_

Реализация этих функций также не содержит ничего сверхестественного:

#include "AppStoreStub.h"

#define PRODUCT_INFO_REQUESTED                               0x0001
#define TRANSACTION_REQUESTED                                0x0002
#define MAX_PRODUCTS_REQUEST                                 5

s3eProductInformationCallbackFn productInfoCallback          = NULL;
s3ePaymentTransactionUpdateCallbackFn transactionCallback    = NULL;
void* billingData                                            = NULL;

uint32 productsCnt = 0;
s3eProductInformation productInformation[MAX_PRODUCTS_REQUEST];
s3ePaymentTransaction transaction;
s3eTransactionReceipt receipt;
uint64 currentTimestamp = 0;
int eventMask = 0;

s3eBool s3eIOSAppStoreBillingAvailableStub() {
    return S3E_TRUE;
}

int32 s3eIOSAppStoreBillingGetIntStub(s3eIOSAppStoreBillingProperty property) {
    switch (property) {
        case S3E_IOSAPPSTOREBILLING_CAN_MAKE_PAYMENTS:
            return 1;
        default:
            return 0;
    }
}

s3eResult s3eIOSAppStoreBillingInitStub(
              s3eProductInformationCallbackFn infoCallback,
              s3ePaymentTransactionUpdateCallbackFn updateCallback,
              void* userData) {
    productInfoCallback = infoCallback;
    transactionCallback = updateCallback;
    billingData = userData;
    return S3E_RESULT_SUCCESS;
}

void s3eIOSAppStoreBillingTerminateStub() {}

s3eResult s3eIOSAppStoreBillingRequestProductInformationStub(
              const char** productIdentifiers,
              uint32 numProductIdentifiers) {
    if (numProductIdentifiers == 0) return S3E_RESULT_ERROR;
    if (numProductIdentifiers >= MAX_PRODUCTS_REQUEST) return S3E_RESULT_ERROR;
    for (uint32 i = 0; i < numProductIdentifiers; i++) {
       memset(&productInformation[i], 0, sizeof(s3eProductInformation));
       strcpy(productInformation[i].m_ProductID, productIdentifiers[i]);
       strcpy(productInformation[i].m_LocalisedTitle, productIdentifiers[i]);
       strcpy(productInformation[i].m_LocalisedDescription, productIdentifiers[i]);
       strcpy(productInformation[i].m_FormattedPrice, "0.00");
       strcpy(productInformation[i].m_PriceLocale, "0.00");
       productInformation[i].m_Price = 0;
       productInformation[i].m_ProductStoreStatus = S3E_PRODUCT_STORE_STATUS_VALID;
    }
    productsCnt = numProductIdentifiers;
    currentTimestamp = s3eTimerGetMs();
    eventMask |= PRODUCT_INFO_REQUESTED;
    return S3E_RESULT_SUCCESS;
}

void s3eIOSAppStoreBillingCancelProductInformationRequestsStub() {}

s3eResult s3eIOSAppStoreBillingRequestPaymentStub(
              s3ePaymentRequest* paymentRequest) {
    memset(&transaction, 0, sizeof(transaction));
    transaction.m_TransactionStatus = S3E_PAYMENT_STATUS_PURCHASED;
    transaction.m_Request = paymentRequest;
    transaction.m_Retain = S3E_FALSE;
    transaction.m_TransactionReceipt = &receipt;
    strcpy(transaction.m_TransactionID, "1");
    strcpy(transaction.m_OriginalTransactionID, "1");
    memset(&receipt, 0, sizeof(receipt));
    receipt.m_ReceiptSize = 0;
    currentTimestamp = s3eTimerGetMs();
    eventMask |= TRANSACTION_REQUESTED;
    return S3E_RESULT_ERROR;  // ???
}

s3eResult s3eIOSAppStoreBillingCompleteTransactionStub(s3e
              PaymentTransaction*  transaction,
              s3eBool finalise) {
    return S3E_RESULT_SUCCESS;
}

s3eResult s3eIOSAppStoreBillingRestoreCompletedTransactionsStub() {
    return S3E_RESULT_SUCCESS;
}

void appStoreStubUpdate() {
#if defined IW_DEBUG
    if (eventMask != 0) {
        if (s3eTimerGetMs() - currentTimestamp < APP_STORE_TIMEOUT) return;
        if (eventMask & PRODUCT_INFO_REQUESTED) {
            for (uint32 i = 0; i < productsCnt; i++) {
                productInfoCallback(&productInformation[i], billingData);
            }
            productsCnt = 0;
        }
        if (eventMask & TRANSACTION_REQUESTED) {
            transactionCallback(&transaction, billingData);
        }
        eventMask = 0;
    }
#endif
}

Здесь следует упомянуть о функции appStoreStubUpdate. Дело в том, что App Store Billing API использует асинхронные вызовы для работы с магазином. Это означает, что вызовы callback функций должны выполняться вне контекста вызова функций запроса информации о продукте s3eIOSAppStoreBillingRequestProductInformation и совершения покупки s3eIOSAppStoreBillingRequestPayment. Эти вызовы мы будем выполнять в appStoreStubUpdate, вызывая последнюю из update-функции Maramalade-приложения. Макрос APP_STORE_TIMEOUT позволяет определять задержку, при обращении к "магазину".

Также, может вызывать вопросы код возврата s3eIOSAppStoreBillingRequestPaymentStub. Честно говоря, мне самому непонятен этот момент. По логике вещей, при успешном выполнении она должна возвращать S3E_RESULT_SUCCESS (равное 0), но в примере используется следующая проверка корректности формирования запроса:

if (s3eIOSAppStoreBillingRequestPayment(&g_PaymentRequest))
     SetStatus("Purchasing %s...", g_ProductID2);
else
     SetStatus("Purchasing %s FAILED", g_ProductID2);

То есть, ожидается ненулевое значение. По всей видимости, разработчики примера допустили ошибку, но на этот момент стоит обратить внимание, при отладке приложения на iPhone.

Последнее, о чем следует упомянуть, это чек. В s3eIOSAppStoreBillingRequestPaymentStub мы формируем пустой чек:

...
memset(&receipt, 0, sizeof(receipt));
receipt.m_ReceiptSize = 0;
...

Но это, возможно, не вполне корректно, если приложение должно верифицировать чек или использовать какую-то информацию, содержащуюся в нем (например дату покупки). О том как строится корректный чек можно прочитать здесь. Здесь описано как выполнить верификацию чека.

Осталось совсем немного. В код примера мы добавляем #include AppStoreStub.h (он обязательно должен идти последним) и вызов appStoreStubUpdate, для эмуляции асинхронных вызовов:

bool ExampleUpdate()
{
    appStoreStubUpdate();
    ...

Скомпилировав пример и запустив его под отладчиком, мы можем убедиться, что теперь мы можем запрашивать информацию о продукте и совершать покупки, совершенно не беспокоя при этом App Store.Разумеется, аналогичным образом можно отлаживать Android-приложения, совершающие внутренние покупки, с использованием Android Market Billing.

суббота, 10 ноября 2012 г.

Работа над ошибками

Как легко сделать ошибку. И насколько-же сложно потом ее бывает найти и исправить! Если кто-то потрудился запустить мой пример, он мог обратить внимание на его странное поведение, заметное даже на эмуляторе. Кнопки не реагируют на нажатие до тех пор, пока мы не завершим касание. После этого, корректным образом, анимируется нажатие кнопки. Если сделать анимацию посложнее, можно заметить, что в момент касания вся активность приложения приостанавливается, а как только мы отпускаем палец, все продолжается, как ни в чем не бывало.

Я не сразу это заметил, а затем еще дольше искал, в чем ошибка. Ошибка здесь:

void Desktop::update(uint64 timestamp) {
    currentTimestamp = timestamp;
    if (isChanged) {
        getScreenSizes();
    }
    int cnt = touchPad.getTouchCount();
    if (cnt > 0) {
        for (int i = 0; i < MAX_TOUCHES; i++) {
            Touch* t = touchPad.getTouch(i);
            if (t->isActive) {
                int msg = (cnt > 1)?emtMultiTouchUp:emtSingleTouchUp;
                if (t->isMoved) {
                    msg = (cnt > 1)?emtMultiTouchMove:emtSingleTouchMove;
                }
                if (t->isPressed) {
                    msg = (cnt > 1)?emtMultiTouchDown:emtSingleTouchDown;
                }
                if (checkBounce(t->id, msg)) return;
                if (currentScene != NULL) {
                    currentScene->sendMessage(msg | (t->id & emtTouchIdMask), 
                                              t->x, t->y);
                }
            }
        }
    }
    if (isKeyAvailable) {
        s3eKeyboardUpdate();
    }
    if (currentScene != NULL) {
        currentScene->update(timestamp);
    }
}


Как всегда, все произошло из за того, что "хотелось как лучше". Дело в том, что если не предпринимать каких-то дополнительных действий, при нажатии, Touchpad (имеется в виду модуль проекта) посылает целую последовательность событий "нажатие", там где кнопке хватило бы всего одного. В результате, кнопка начинала "дребезжать", быстро нажимаясь и отжимаясь. Чтобы этого избежать, была добавлена функция checkBounce, формирующая правильную последовательность событий, убирая лишние. В процессе поиска ошибки, ее код был перепроверен, наверное, раз десять. А ошибка была совсем не там.

Вот правильный код:

void Desktop::update(uint64 timestamp) {
    currentTimestamp = timestamp;
    if (isChanged) {
        getScreenSizes();
    }
    int cnt = touchPad.getTouchCount();
    if (cnt > 0) {
        for (int i = 0; i < MAX_TOUCHES; i++) {
            Touch* t = touchPad.getTouch(i);
            if (t->isActive) {
                int msg = (cnt > 1)?emtMultiTouchUp:emtSingleTouchUp;
                if (t->isMoved) {
                    msg = (cnt > 1)?emtMultiTouchMove:emtSingleTouchMove;
                }
                if (t->isPressed) {
                    msg = (cnt > 1)?emtMultiTouchDown:emtSingleTouchDown;
                }
                if (!checkBounce(t->id, msg)) {
                   if (currentScene != NULL) {
                       currentScene->sendMessage(msg | (t->id & emtTouchIdMask), 
                                                 t->x, t->y);
                   }
                }
            }
        }
    }
    if (isKeyAvailable) {
        s3eKeyboardUpdate();
    }
    if (currentScene != NULL) {
        currentScene->update(timestamp);
    }
}


Найдите десять отличий ;)

Для тех, кто не следил за циклом статей, как всегда, ссылка на исправленный проект.

P.S. В эти выходные закончился Evaluation период у Мармелада. Я решил его продлить, теперь уже за денежку :) Считаю, что $149 совсем не дорого за такое удовольствие.