블로그

  • Android studio Gradle 프로젝트 생성

    Hello World 안드로이드 스튜디오 프로젝트 생성

    안드로이드 스튜디오를 실행합니다. 그리고 File -> new -> New Project를 누릅니다. 음 API24 정도로 선택합니다. Activity 는 Blank Activity로 선택하겠습니다. 저흰 액티비티가 중요한게 아니니깐요.

     그리고 프로젝트 표시는 Android View로 하도록 하겠습니다. 왜냐하면 안드로이드 앱의 Gradle 스크립트를 잘표현해주기 때문입니다.

    Gradle 구성요소 소개

    (1) app모듈 안드로이드 스튜디오는 멀티프로젝트를 생성하게 됩니다. Gradle 프로젝트 하위에는 적어도 1개 이상의 모듈을 포합합니다. 최초 생성되는 모듈 이름은 app입니다. 새로운 모듈을 추가할때는
    File -> New -> New Module 
    을 선택하면됩니다.
    (2) manifest 폴더AndroidManifest.xml 파일을 표시합니다. 앱이름과 권한 과 같은 프로젝트 메타정보를 담고 있습니다. 모듈별로 AndroidManifest.xml 파일을 포함하게 됩니다.
    (3) java 폴더소스코드와 테스트 코드가 있습니다.
    (4) Gradle Scripts안드로이드 스튜디오에서의 gradle sciprt을 포함하고있습니다.

    프로젝트 build.gradle (여기선 Project: HelloWorld)

    // Top-level build file where you can add configuration options common to all sub-projects/modules.
    
    buildscript {
        repositories {
            jcenter()
        }
        dependencies {
            classpath 'com.android.tools.build:gradle:2.3.3'
    
            // NOTE: Do not place your application dependencies here; they belong
            // in the individual module build.gradle files
        }
    }
    
    allprojects {
        repositories {
            jcenter()
        }
    }
    
    task clean(type: Delete) {
        delete rootProject.buildDir
    } 

    직접 까서 보겠습니다.프로젝트 build gradle은 다수의 모듈이 존재할떄 전체 모듈에 공통적으로 적용하는 부분을 기술하는 부분입니다.
    크게 두부분으로 구성되어있습니다. buildscirpt은 빌드 스크립트를 구동하는 부분입니다. 외부저장소와 의존성 부분을 지정합니다. 외부저장소로 jcenter, mavenCentral 이 있습니다.요즘은 jcenter를 많이 사용한다고 합니다. 또한 dependencies(의존성부분) 에는 안드로이드 gradle의 플로그인 버전을 기술합니다.
    그외에는 전체 프로젝트 공통으로 사용할수 있는 task 를 정의합니다. 기본적으로 clean 태스크가 추가되며, 단순히 build 폴저를 제거하는 역할을 하고있습니다. app과 같은 하위 Module의 build 폴더도 모두 제거한답니다. 모듈이 다수이면 다수모듈의 build 폴더가 제거되겠지요?

    모듈 build.gradle (여기선 Module:app)

    apply plugin: 'com.android.application'
    
    android {
        compileSdkVersion 26
        buildToolsVersion "26.0.0"
        defaultConfig {
            applicationId "com.example.pjh.helloworld"
            minSdkVersion 24
            targetSdkVersion 26
            versionCode 1
            versionName "1.0"
            testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
        }
        buildTypes {
            release {
                minifyEnabled false
                proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            }
        }
    }
    
    dependencies {
        compile fileTree(dir: 'libs', include: ['*.jar'])
        androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
            exclude group: 'com.android.support', module: 'support-annotations'
        })
        compile 'com.android.support:appcompat-v7:26.+'
        compile 'com.android.support.constraint:constraint-layout:1.0.2'
        testCompile 'junit:junit:4.12'
    } 

    크게 네 부분으로 구분됩니다. 
    첫번째는 모듈의 plugin 부분입니다.안드로이드 App module 은 com.android.application을 지정합니다.안드로이드 library module은 com.android.library를 지정합니다.동시에 플러그인을 2개 지정할수는 당연히 없습니다.
    두번째는 android로 AndroidManifest.xml을 재설정 한다고 생각하시면 됩니다.gradle에서 설정한게 manifest보다 우선시 됩니다. 
    세번째는 buildTypes에는 빌드 타입에 따라 다른 동작을 지정할 수 있습니다.빌드타입에 따라 다른 동작을 지정할 수 있습니다. debug와 release가 있습니다. debug는 개발단계에서 사용되며, release는 마켓이나 외부에 배포할때 사용합니다. 
    네번째는 의존성부분(dependencies) 입니다.libs폴더에있는 모든 jar파일 의존성에 추가합니다.또한 compile은 module을 빌드 할 때 포함하는 외부라이브러리입니다. 로컬에 존재하지 않는 경우, 앞서 지정한 저장소였던 jcenter(프로젝트 gradle에서) appcompat 지원 라이브러리를 다운로드합니다.

  • Gradle 개요

    Gradle OverView

    Gradle은 Gradle 사에서 만든 범용 빌드 도구중에 하나입니다. 안드로이드에서 빌드 뿐만 아니라, java, c/c++ 등의 모든 범용 언어를 지원합니다. 그러므로 엄청 강력한 툴이지요. 그래서 Gradle을 한번 배워두면 다른언어로 개발하더라도 빌드 스크립트를 처음부터 다시 작성할 필요없이 재사용하실 수 있습니다. 누차 강조하지만 꼭 gradle을 배워보도록 합시다.

    다음은 gradle의 주요 특징 4가지 입니다.

    1. Polyglot Build

    Gradle은 각 언어를 플러그인으로 구별합니다. java는 java, java 웹 프로젝트는 war, 안드로이드 앱은 com.android.application 플로그인을 사용하면 됩니다.

    2. 도구통합

    gradle은 이클립스, 안드로이드 스튜디오 와 같은 IDE에서 정말 편리하게 사용할 수 있도록 창을 제공하고있습니다. 또한 젠킨스와 함께 활용할 수 도있습니다. 소스코드가 git에 업로드되면 서버에서 CheckStyle, FindBugs 등의 플러그인을 활용하여 소스코드가 잠재적으로 가진 문제를 검출하여 개발자에게 통보하거나 위험한 코 드를 merge할 수 없도록 강제적으로 할 수 있습니다.

    3. 외부 라이브러리 관리 자동화

    Gradle의 또다른 장점중 하나는 개발자가 더는 외부라이브러리를 관리하지 않아도 된다는 점입니다. 개인적으로 이점이 가장 마음에 듭니다. 이클립스에서 개발했던 과거의 경우에는 libs 폴더에 원하는 외부라이브러리 파일을 직접 복사하였지만, Gradle 에서는 단순히 외부 저장소 위치와 라이브러리의 그룹, 이름, 버전 등을 지정해주면 알아서 다운로드하고 빌드에 포함시키게 됩니다. 이얼마나 편하고 좋습니까.

    4. 고성능 빌드

    Gradle은 점진적 빌드, 빌드캐싱, 병렬 빌드 기능을 지원하는 고성능 빌드를 지향합니다. 하지만 실제로 우니도우 환경에서 안드로이드 앱을 빌드해보면 gradle 메모리 사용량이 많고 빌드시 CPU 점유율이 엄청 높게 잡힙니다. 실무에서 권장하는 사양은 메모리 8GB와 SSD 장착입니다. 메모리 4GB에서 모듈의 수가 늘어나면 Out of memery 현상이 발생합니다. 또한 heap이 부족하다고 난리치기도 한답니다.(진짜 경험)

    안드로이드 스튜디오에서 Gradle의 특징을 살펴보겠습니다.

    1. 멀티 프로젝트 구조

    안드로이드 스튜디오에서 프로젝트를 생성하면 멀티 프로젝트로 생성됩니다. app이라는 폴더가 있는데 이를 Gradle에서는 모듈이라고 부릅니다. gradle에서는 app 모듈뿐만 아니라 새로운 모듈을 추가하여 모듈별로 src 폴더를 포함하게 됩니다.

    2. src폴더 구조가 다름

    androidTest, main, test 폴더를 확인하실 수 있습니다. Test 폴더는 Local Unit Test 를 지원합니다.

    3. libs 폴더

    이클립스 libs 폴더에는 빌드하는데 필요한 외부라이브러리를 직접 포함시켜야 했습니다. 하지만 gradle에서는 의존성 관리를 gradle이 담당하므로 libs 폴더를 사용하지 않아도됩니다. 필요한 스크립트 파일에서 외부라이브러리의 저장소와 버전등을 지정하면 빌드할 때 알아서 해당 버전을 다운로드하여 포함합니다. 또한, +옵션등을 적용하면 최신버전을 자동으로 다운로드 할 수 있습니다.

    4. bin 폴더

    gradle에서 빌드를하면 build/output/apk 폴더에 apk 파일이 위치하게됩니다. 혹은 AAR파일도 있게 됩니다.

  • In-app Billing API v3에서 IllegalStateException이 발생

    TrivialDrive 샘플 앱을 탐색하여 Android 인앱 결제 API 버전 3을 구현할 때 Google의 지침을 따랐다면 아마도 다음과 같은 수많은 IllegalStateException을 접했을 것입니다.

    java.lang.IllegalStateException: Can't start async operation (consume) because another async operation(consume) is in progress.
    ...
    java.lang.IllegalStateException: IAB helper is not set up. Can't perform operation: queryInventory
    ...
    java.lang.IllegalStateException: IabHelper was disposed of, so it cannot be used.
    

    이러한 예외가 발생하는 이유는 TrivialDrive 샘플 앱의 IabHelper 클래스에 있습니다. 이 클래스는 동일한 변수(mSetupDone, mDisposed, mAsyncInProgress 등)를 조작하는 여러 스레드를 사용하며, CPU 캐싱으로 인해 한 스레드에 의해 변경된 변수가 다른 스레드에 표시된다는 보장은 없습니다. 이 시나리오를 방지하려면 이러한 멤버 변수를 휘발성으로 선언해야 합니다  .

    이 클래스의 dispose() 함수에도 문제가 있습니다. 서비스가 startSetup() 메서드에 바인딩되기 전에 dispose()가 호출된 경우 unbindService에 대한 호출에서 IllegalArgumentException이 발생할 수 있습니다. 설정을 시작했지만 사용자가 뒤로 버튼을 클릭하고 활동을 닫으면 어떤 일이 발생하는지 생각해 보세요. 이를 처리하려면 dispose() 메서드에서 해당 예외를 포착해야 합니다.

    이 클래스의 또 다른 문제는 여러 메서드에서 RuntimeExcptions(IllegalStateException)를 발생시키는 잘못된 선택입니다. 대부분의 경우 자신의 코드에서 RuntimeExeptions를 발생시키는 것은 확인되지 않은 예외 이기 때문에 바람직하지 않습니다  . 이는 자신의 애플리케이션을 방해하는 것과 같습니다. 포착되지 않으면 이러한 예외로 인해 앱이 중단됩니다.

    이에 대한 해결책은 자체  확인 예외를 생성  하고 IllegalStateException 대신 이를 발생시키도록 IabHelper 클래스를 변경하는 것입니다. 그러면 컴파일 타임에 코드에서 예외가 발생할 수 있는 모든 곳에서 이 예외를 처리해야 합니다.

    내 사용자 정의 예외는 다음과 같습니다.

    public class MyIllegalStateException extends Exception {
    	
        private static final long serialVersionUID = 1L;
    	
    	//Parameterless Constructor
        public MyIllegalStateException() {}
    
        //Constructor that accepts a message
        public MyIllegalStateException(String message)
        {
           super(message);
        }
    }
    

    이제 IabHelper 클래스를 변경해 보겠습니다. 필요한 변수를 휘발성으로 선언하고, dispose 메서드를 수정하고, 자체적으로 확인된 예외를 발생시킵니다. 대부분의 경우 IabHelper 클래스의 메서드에서 버블링되도록 하고, 이러한 메서드를 호출하는 코드에서 이를 처리할 수 있습니다. 예를 들어 인앱 구매를 처리하는 활동이나 프래그먼트에서 다음을 수행합니다.

    try {
       setUpBilling(targetActivityInstance.allData.getAll());
    } catch (MyIllegalStateException ex) {
        ex.printStackTrace();
    }
    

    그리고 전체 클래스는 다음과 같습니다.

    /* Copyright (c) 2012 Google Inc.
     *
     * Licensed under the Apache License, Version 2.0 (the "License");
     * you may not use this file except in compliance with the License.
     * You may obtain a copy of the License at
     *
     *     http://www.apache.org/licenses/LICENSE-2.0
     *
     * Unless required by applicable law or agreed to in writing, software
     * distributed under the License is distributed on an "AS IS" BASIS,
     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     * See the License for the specific language governing permissions and
     * limitations under the License.
     */
    
    /**
     * Provides convenience methods for in-app billing. You can create one instance of this
     * class for your application and use it to process in-app billing operations.
     * It provides synchronous (blocking) and asynchronous (non-blocking) methods for
     * many common in-app billing operations, as well as automatic signature
     * verification.
     *
     * After instantiating, you must perform setup in order to start using the object.
     * To perform setup, call the {@link #startSetup} method and provide a listener;
     * that listener will be notified when setup is complete, after which (and not before)
     * you may call other methods.
     *
     * After setup is complete, you will typically want to request an inventory of owned
     * items and subscriptions. See {@link #queryInventory}, {@link #queryInventoryAsync}
     * and related methods.
     *
     * When you are done with this object, don't forget to call {@link #dispose}
     * to ensure proper cleanup. This object holds a binding to the in-app billing
     * service, which will leak unless you dispose of it correctly. If you created
     * the object on an Activity's onCreate method, then the recommended
     * place to dispose of it is the Activity's onDestroy method.
     *
     * A note about threading: When using this object from a background thread, you may
     * call the blocking versions of methods; when using from a UI thread, call
     * only the asynchronous versions and handle the results via callbacks.
     * Also, notice that you can only call one asynchronous operation at a time;
     * attempting to start a second asynchronous operation while the first one
     * has not yet completed will result in an exception being thrown.
     *
     * @author Bruno Oliveira (Google)
     *
     */
    public class IabHelper {
        // Is debug logging enabled?
        boolean mDebugLog = false;
        String mDebugTag = "IabHelper";
    
        // Is setup done?
        volatile boolean mSetupDone = false;
    
        // Has this object been disposed of? (If so, we should ignore callbacks, etc)
        volatile boolean mDisposed = false;
    
        // Are subscriptions supported?
        volatile boolean mSubscriptionsSupported = false;
    
        // Is an asynchronous operation in progress?
        // (only one at a time can be in progress)
        volatile boolean mAsyncInProgress = false;
    
        // (for logging/debugging)
        // if mAsyncInProgress == true, what asynchronous operation is in progress?
        String mAsyncOperation = "";
    
        // Context we were passed during initialization
        Context mContext;
    
        // Connection to the service
        IInAppBillingService mService;
        ServiceConnection mServiceConn;
    
        // The request code used to launch purchase flow
        int mRequestCode;
    
        // The item type of the current purchase flow
        String mPurchasingItemType;
    
        // Public key for verifying signature, in base64 encoding
        String mSignatureBase64 = null;
    
        // Billing response codes
        public static final int BILLING_RESPONSE_RESULT_OK = 0;
        public static final int BILLING_RESPONSE_RESULT_USER_CANCELED = 1;
        public static final int BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE = 3;
        public static final int BILLING_RESPONSE_RESULT_ITEM_UNAVAILABLE = 4;
        public static final int BILLING_RESPONSE_RESULT_DEVELOPER_ERROR = 5;
        public static final int BILLING_RESPONSE_RESULT_ERROR = 6;
        public static final int BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED = 7;
        public static final int BILLING_RESPONSE_RESULT_ITEM_NOT_OWNED = 8;
    
        // IAB Helper error codes
        public static final int IABHELPER_ERROR_BASE = -1000;
        public static final int IABHELPER_REMOTE_EXCEPTION = -1001;
        public static final int IABHELPER_BAD_RESPONSE = -1002;
        public static final int IABHELPER_VERIFICATION_FAILED = -1003;
        public static final int IABHELPER_SEND_INTENT_FAILED = -1004;
        public static final int IABHELPER_USER_CANCELLED = -1005;
        public static final int IABHELPER_UNKNOWN_PURCHASE_RESPONSE = -1006;
        public static final int IABHELPER_MISSING_TOKEN = -1007;
        public static final int IABHELPER_UNKNOWN_ERROR = -1008;
        public static final int IABHELPER_SUBSCRIPTIONS_NOT_AVAILABLE = -1009;
        public static final int IABHELPER_INVALID_CONSUMPTION = -1010;
    
        // Keys for the responses from InAppBillingService
        public static final String RESPONSE_CODE = "RESPONSE_CODE";
        public static final String RESPONSE_GET_SKU_DETAILS_LIST = "DETAILS_LIST";
        public static final String RESPONSE_BUY_INTENT = "BUY_INTENT";
        public static final String RESPONSE_INAPP_PURCHASE_DATA = "INAPP_PURCHASE_DATA";
        public static final String RESPONSE_INAPP_SIGNATURE = "INAPP_DATA_SIGNATURE";
        public static final String RESPONSE_INAPP_ITEM_LIST = "INAPP_PURCHASE_ITEM_LIST";
        public static final String RESPONSE_INAPP_PURCHASE_DATA_LIST = "INAPP_PURCHASE_DATA_LIST";
        public static final String RESPONSE_INAPP_SIGNATURE_LIST = "INAPP_DATA_SIGNATURE_LIST";
        public static final String INAPP_CONTINUATION_TOKEN = "INAPP_CONTINUATION_TOKEN";
    
        // Item types
        public static final String ITEM_TYPE_INAPP = "inapp";
        public static final String ITEM_TYPE_SUBS = "subs";
    
        // some fields on the getSkuDetails response bundle
        public static final String GET_SKU_DETAILS_ITEM_LIST = "ITEM_ID_LIST";
        public static final String GET_SKU_DETAILS_ITEM_TYPE_LIST = "ITEM_TYPE_LIST";
    
        /**
         * Creates an instance. After creation, it will not yet be ready to use. You must perform
         * setup by calling {@link #startSetup} and wait for setup to complete. This constructor does not
         * block and is safe to call from a UI thread.
         *
         * @param ctx Your application or Activity context. Needed to bind to the in-app billing service.
         * @param base64PublicKey Your application's public key, encoded in base64.
         *     This is used for verification of purchase signatures. You can find your app's base64-encoded
         *     public key in your application's page on Google Play Developer Console. Note that this
         *     is NOT your "developer public key".
         */
        public IabHelper(Context ctx, String base64PublicKey) {
            mContext = ctx.getApplicationContext();
            mSignatureBase64 = base64PublicKey;
            logDebug("IAB helper created.");
        }
    
        /**
         * Enables or disable debug logging through LogCat.
         */
        public void enableDebugLogging(boolean enable, String tag) throws MyIllegalStateException {
            checkNotDisposed();
            mDebugLog = enable;
            mDebugTag = tag;
        }
    
        public void enableDebugLogging(boolean enable) throws MyIllegalStateException {
            checkNotDisposed();
            mDebugLog = enable;
        }
    
        /**
         * Callback for setup process. This listener's {@link #onIabSetupFinished} method is called
         * when the setup process is complete.
         */
        public interface OnIabSetupFinishedListener {
            /**
             * Called to notify that setup is complete.
             *
             * @param result The result of the setup process.
             */
            public void onIabSetupFinished(IabResult result);
        }
    
        /**
         * Starts the setup process. This will start up the setup process asynchronously.
         * You will be notified through the listener when the setup process is complete.
         * This method is safe to call from a UI thread.
         *
         * @param listener The listener to notify when the setup process is complete.
         */
        public void startSetup(final OnIabSetupFinishedListener listener) throws MyIllegalStateException {
            // If already set up, can't do it again.
            checkNotDisposed();
            if (mSetupDone) throw new MyIllegalStateException("IAB helper is already set up.");
    
            // Connection to IAB service
            logDebug("Starting in-app billing setup.");
            mServiceConn = new ServiceConnection() {
                @Override
                public void onServiceDisconnected(ComponentName name) {
                    logDebug("Billing service disconnected.");
                    mService = null;
                }
    
                @Override
                public void onServiceConnected(ComponentName name, IBinder service) {
                    if (mDisposed) return;
                    logDebug("Billing service connected.");
                    mService = IInAppBillingService.Stub.asInterface(service);
                    String packageName = mContext.getPackageName();
                    try {
                        logDebug("Checking for in-app billing 3 support.");
    
                        // check for in-app billing v3 support
                        int response = mService.isBillingSupported(3, packageName, ITEM_TYPE_INAPP);
                        if (response != BILLING_RESPONSE_RESULT_OK) {
                            if (listener != null) listener.onIabSetupFinished(new IabResult(response,
                                    "Error checking for billing v3 support."));
    
                            // if in-app purchases aren't supported, neither are subscriptions.
                            mSubscriptionsSupported = false;
                            return;
                        }
                        logDebug("In-app billing version 3 supported for " + packageName);
    
                        // check for v3 subscriptions support
                        response = mService.isBillingSupported(3, packageName, ITEM_TYPE_SUBS);
                        if (response == BILLING_RESPONSE_RESULT_OK) {
                            logDebug("Subscriptions AVAILABLE.");
                            mSubscriptionsSupported = true;
                        }
                        else {
                            logDebug("Subscriptions NOT AVAILABLE. Response: " + response);
                        }
    
                        mSetupDone = true;
                    }
                    catch (RemoteException e) {
                        if (listener != null) {
                            listener.onIabSetupFinished(new IabResult(IABHELPER_REMOTE_EXCEPTION,
                                                        "RemoteException while setting up in-app billing."));
                        }
                        e.printStackTrace();
                        return;
                    }
    
                    if (listener != null) {
                        listener.onIabSetupFinished(new IabResult(BILLING_RESPONSE_RESULT_OK, "Setup successful."));
                    }
                }
            };
    
            Intent serviceIntent = new Intent("com.android.vending.billing.InAppBillingService.BIND");
            serviceIntent.setPackage("com.android.vending");
            if (!mContext.getPackageManager().queryIntentServices(serviceIntent, 0).isEmpty()) {
                // service available to handle that Intent
                mContext.bindService(serviceIntent, mServiceConn, Context.BIND_AUTO_CREATE);
            }
            else {
                // no service available to handle that Intent
                if (listener != null) {
                    listener.onIabSetupFinished(
                            new IabResult(BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE,
                            "Billing service unavailable on device."));
                }
            }
        }
    
        /**
         * Dispose of object, releasing resources. It's very important to call this
         * method when you are done with this object. It will release any resources
         * used by it such as service connections. Naturally, once the object is
         * disposed of, it can't be used again.
         */
        public void dispose() {
            logDebug("Disposing.");
            mSetupDone = false;
            if (mServiceConn != null) {
                logDebug("Unbinding from service.");
                try{
                    if (mContext != null) mContext.unbindService(mServiceConn);
                } catch(IllegalArgumentException ex){ //ADDED THIS CATCH
                    //somehow, the service was already unregistered
                	ex.printStackTrace();
                }
            }
            mDisposed = true;
            mContext = null;
            mServiceConn = null;
            mService = null;
            mPurchaseListener = null;
        }
    
        private void checkNotDisposed() throws MyIllegalStateException {
            if (mDisposed) throw new MyIllegalStateException("IabHelper was disposed of, so it cannot be used.");
        }
    
        /** Returns whether subscriptions are supported. */
        public boolean subscriptionsSupported() throws MyIllegalStateException {
            checkNotDisposed();
            return mSubscriptionsSupported;
        }
    
    
        /**
         * Callback that notifies when a purchase is finished.
         */
        public interface OnIabPurchaseFinishedListener {
            /**
             * Called to notify that an in-app purchase finished. If the purchase was successful,
             * then the sku parameter specifies which item was purchased. If the purchase failed,
             * the sku and extraData parameters may or may not be null, depending on how far the purchase
             * process went.
             *
             * @param result The result of the purchase.
             * @param info The purchase information (null if purchase failed)
             */
            public void onIabPurchaseFinished(IabResult result, Purchase info);
        }
    
        // The listener registered on launchPurchaseFlow, which we have to call back when
        // the purchase finishes
        OnIabPurchaseFinishedListener mPurchaseListener;
    
        public void launchPurchaseFlow(Activity act, String sku, int requestCode, OnIabPurchaseFinishedListener listener) throws MyIllegalStateException {
            launchPurchaseFlow(act, sku, requestCode, listener, "");
        }
    
        public void launchPurchaseFlow(Activity act, String sku, int requestCode,
                OnIabPurchaseFinishedListener listener, String extraData) throws MyIllegalStateException {
            launchPurchaseFlow(act, sku, ITEM_TYPE_INAPP, requestCode, listener, extraData);
        }
    
        public void launchSubscriptionPurchaseFlow(Activity act, String sku, int requestCode,
                OnIabPurchaseFinishedListener listener) throws MyIllegalStateException {
            launchSubscriptionPurchaseFlow(act, sku, requestCode, listener, "");
        }
    
        public void launchSubscriptionPurchaseFlow(Activity act, String sku, int requestCode,
                OnIabPurchaseFinishedListener listener, String extraData) throws MyIllegalStateException {
            launchPurchaseFlow(act, sku, ITEM_TYPE_SUBS, requestCode, listener, extraData);
        }
    
        /**
         * Initiate the UI flow for an in-app purchase. Call this method to initiate an in-app purchase,
         * which will involve bringing up the Google Play screen. The calling activity will be paused while
         * the user interacts with Google Play, and the result will be delivered via the activity's
         * {@link android.app.Activity#onActivityResult} method, at which point you must call
         * this object's {@link #handleActivityResult} method to continue the purchase flow. This method
         * MUST be called from the UI thread of the Activity.
         *
         * @param act The calling activity.
         * @param sku The sku of the item to purchase.
         * @param itemType indicates if it's a product or a subscription (ITEM_TYPE_INAPP or ITEM_TYPE_SUBS)
         * @param requestCode A request code (to differentiate from other responses --
         *     as in {@link android.app.Activity#startActivityForResult}).
         * @param listener The listener to notify when the purchase process finishes
         * @param extraData Extra data (developer payload), which will be returned with the purchase data
         *     when the purchase completes. This extra data will be permanently bound to that purchase
         *     and will always be returned when the purchase is queried.
         */
        public void launchPurchaseFlow(Activity act, String sku, String itemType, int requestCode,
                            OnIabPurchaseFinishedListener listener, String extraData) throws MyIllegalStateException {
            checkNotDisposed();
            checkSetupDone("launchPurchaseFlow");
            flagStartAsync("launchPurchaseFlow");
            IabResult result;
    
            if (itemType.equals(ITEM_TYPE_SUBS) && !mSubscriptionsSupported) {
                IabResult r = new IabResult(IABHELPER_SUBSCRIPTIONS_NOT_AVAILABLE,
                        "Subscriptions are not available.");
                flagEndAsync();
                if (listener != null) listener.onIabPurchaseFinished(r, null);
                return;
            }
    
            try {
                logDebug("Constructing buy intent for " + sku + ", item type: " + itemType);
                Bundle buyIntentBundle = mService.getBuyIntent(3, mContext.getPackageName(), sku, itemType, extraData);
                int response = getResponseCodeFromBundle(buyIntentBundle);
                if (response != BILLING_RESPONSE_RESULT_OK) {
                    logError("Unable to buy item, Error response: " + getResponseDesc(response));
                    flagEndAsync();
                    result = new IabResult(response, "Unable to buy item");
                    if (listener != null) listener.onIabPurchaseFinished(result, null);
                    return;
                }
    
                PendingIntent pendingIntent = buyIntentBundle.getParcelable(RESPONSE_BUY_INTENT);
                logDebug("Launching buy intent for " + sku + ". Request code: " + requestCode);
                mRequestCode = requestCode;
                mPurchaseListener = listener;
                mPurchasingItemType = itemType;
                act.startIntentSenderForResult(pendingIntent.getIntentSender(),
                                               requestCode, new Intent(),
                                               Integer.valueOf(0), Integer.valueOf(0),
                                               Integer.valueOf(0));
            }
            catch (SendIntentException e) {
                logError("SendIntentException while launching purchase flow for sku " + sku);
                e.printStackTrace();
                flagEndAsync();
    
                result = new IabResult(IABHELPER_SEND_INTENT_FAILED, "Failed to send intent.");
                if (listener != null) listener.onIabPurchaseFinished(result, null);
            }
            catch (RemoteException e) {
                logError("RemoteException while launching purchase flow for sku " + sku);
                e.printStackTrace();
                flagEndAsync();
    
                result = new IabResult(IABHELPER_REMOTE_EXCEPTION, "Remote exception while starting purchase flow");
                if (listener != null) listener.onIabPurchaseFinished(result, null);
            }
        }
    
        /**
         * Handles an activity result that's part of the purchase flow in in-app billing. If you
         * are calling {@link #launchPurchaseFlow}, then you must call this method from your
         * Activity's {@link android.app.Activity@onActivityResult} method. This method
         * MUST be called from the UI thread of the Activity.
         *
         * @param requestCode The requestCode as you received it.
         * @param resultCode The resultCode as you received it.
         * @param data The data (Intent) as you received it.
         * @return Returns true if the result was related to a purchase flow and was handled;
         *     false if the result was not related to a purchase, in which case you should
         *     handle it normally.
         */
        public boolean handleActivityResult(int requestCode, int resultCode, Intent data) throws MyIllegalStateException {
            IabResult result;
            if (requestCode != mRequestCode) return false;
    
            checkNotDisposed();
            checkSetupDone("handleActivityResult");
    
            // end of async purchase operation that started on launchPurchaseFlow
            flagEndAsync();
    
            if (data == null) {
                logError("Null data in IAB activity result.");
                result = new IabResult(IABHELPER_BAD_RESPONSE, "Null data in IAB result");
                if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
                return true;
            }
    
            int responseCode = getResponseCodeFromIntent(data);
            String purchaseData = data.getStringExtra(RESPONSE_INAPP_PURCHASE_DATA);
            String dataSignature = data.getStringExtra(RESPONSE_INAPP_SIGNATURE);
    
            if (resultCode == Activity.RESULT_OK && responseCode == BILLING_RESPONSE_RESULT_OK) {
                logDebug("Successful resultcode from purchase activity.");
                logDebug("Purchase data: " + purchaseData);
                logDebug("Data signature: " + dataSignature);
                logDebug("Extras: " + data.getExtras());
                logDebug("Expected item type: " + mPurchasingItemType);
    
                if (purchaseData == null || dataSignature == null) {
                    logError("BUG: either purchaseData or dataSignature is null.");
                    logDebug("Extras: " + data.getExtras().toString());
                    result = new IabResult(IABHELPER_UNKNOWN_ERROR, "IAB returned null purchaseData or dataSignature");
                    if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
                    return true;
                }
    
                Purchase purchase = null;
                try {
                    purchase = new Purchase(mPurchasingItemType, purchaseData, dataSignature);
                    String sku = purchase.getSku();
    
                    // Verify signature
                    if (!Security.verifyPurchase(mSignatureBase64, purchaseData, dataSignature)) {
                        logError("Purchase signature verification FAILED for sku " + sku);
                        result = new IabResult(IABHELPER_VERIFICATION_FAILED, "Signature verification failed for sku " + sku);
                        if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, purchase);
                        return true;
                    }
                    logDebug("Purchase signature successfully verified.");
                }
                catch (JSONException e) {
                    logError("Failed to parse purchase data.");
                    e.printStackTrace();
                    result = new IabResult(IABHELPER_BAD_RESPONSE, "Failed to parse purchase data.");
                    if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
                    return true;
                }
    
                if (mPurchaseListener != null) {
                    mPurchaseListener.onIabPurchaseFinished(new IabResult(BILLING_RESPONSE_RESULT_OK, "Success"), purchase);
                }
            }
            else if (resultCode == Activity.RESULT_OK) {
                // result code was OK, but in-app billing response was not OK.
                logDebug("Result code was OK but in-app billing response was not OK: " + getResponseDesc(responseCode));
                if (mPurchaseListener != null) {
                    result = new IabResult(responseCode, "Problem purchashing item.");
                    mPurchaseListener.onIabPurchaseFinished(result, null);
                }
            }
            else if (resultCode == Activity.RESULT_CANCELED) {
                logDebug("Purchase canceled - Response: " + getResponseDesc(responseCode));
                result = new IabResult(IABHELPER_USER_CANCELLED, "User canceled.");
                if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
            }
            else {
                logError("Purchase failed. Result code: " + Integer.toString(resultCode)
                        + ". Response: " + getResponseDesc(responseCode));
                result = new IabResult(IABHELPER_UNKNOWN_PURCHASE_RESPONSE, "Unknown purchase response.");
                if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
            }
            return true;
        }
    
        public Inventory queryInventory(boolean querySkuDetails, List moreSkus) throws IabException, MyIllegalStateException {
            return queryInventory(querySkuDetails, moreSkus, null);
        }
    
        /**
         * Queries the inventory. This will query all owned items from the server, as well as
         * information on additional skus, if specified. This method may block or take long to execute.
         * Do not call from a UI thread. For that, use the non-blocking version {@link #refreshInventoryAsync}.
         *
         * @param querySkuDetails if true, SKU details (price, description, etc) will be queried as well
         *     as purchase information.
         * @param moreItemSkus additional PRODUCT skus to query information on, regardless of ownership.
         *     Ignored if null or if querySkuDetails is false.
         * @param moreSubsSkus additional SUBSCRIPTIONS skus to query information on, regardless of ownership.
         *     Ignored if null or if querySkuDetails is false.
         * @throws IabException if a problem occurs while refreshing the inventory.
         */
        public Inventory queryInventory(boolean querySkuDetails, List moreItemSkus,
                                            List moreSubsSkus) throws IabException, MyIllegalStateException {
            checkNotDisposed();
            checkSetupDone("queryInventory");
            try {
                Inventory inv = new Inventory();
                int r = queryPurchases(inv, ITEM_TYPE_INAPP);
                if (r != BILLING_RESPONSE_RESULT_OK) {
                    throw new IabException(r, "Error refreshing inventory (querying owned items).");
                }
    
                if (querySkuDetails) {
                    r = querySkuDetails(ITEM_TYPE_INAPP, inv, moreItemSkus);
                    if (r != BILLING_RESPONSE_RESULT_OK) {
                        throw new IabException(r, "Error refreshing inventory (querying prices of items).");
                    }
                }
    
                // if subscriptions are supported, then also query for subscriptions
                if (mSubscriptionsSupported) {
                    r = queryPurchases(inv, ITEM_TYPE_SUBS);
                    if (r != BILLING_RESPONSE_RESULT_OK) {
                        throw new IabException(r, "Error refreshing inventory (querying owned subscriptions).");
                    }
    
                    if (querySkuDetails) {
                        r = querySkuDetails(ITEM_TYPE_SUBS, inv, moreItemSkus);
                        if (r != BILLING_RESPONSE_RESULT_OK) {
                            throw new IabException(r, "Error refreshing inventory (querying prices of subscriptions).");
                        }
                    }
                }
    
                return inv;
            }
            catch (RemoteException e) {
                throw new IabException(IABHELPER_REMOTE_EXCEPTION, "Remote exception while refreshing inventory.", e);
            }
            catch (JSONException e) {
                throw new IabException(IABHELPER_BAD_RESPONSE, "Error parsing JSON response while refreshing inventory.", e);
            }
        }
    
        /**
         * Listener that notifies when an inventory query operation completes.
         */
        public interface QueryInventoryFinishedListener {
            /**
             * Called to notify that an inventory query operation completed.
             *
             * @param result The result of the operation.
             * @param inv The inventory.
             */
            public void onQueryInventoryFinished(IabResult result, Inventory inv);
        }
    
    
        /**
         * Asynchronous wrapper for inventory query. This will perform an inventory
         * query as described in {@link #queryInventory}, but will do so asynchronously
         * and call back the specified listener upon completion. This method is safe to
         * call from a UI thread.
         *
         * @param querySkuDetails as in {@link #queryInventory}
         * @param moreSkus as in {@link #queryInventory}
         * @param listener The listener to notify when the refresh operation completes.
         */
        public void queryInventoryAsync(final boolean querySkuDetails,
                                   final List moreSkus,
                                   final QueryInventoryFinishedListener listener)  throws MyIllegalStateException {
            final Handler handler = new Handler();
            checkNotDisposed();
            checkSetupDone("queryInventory");
            flagStartAsync("refresh inventory");
            (new Thread(new Runnable() {
                public void run() {
                    IabResult result = new IabResult(BILLING_RESPONSE_RESULT_OK, "Inventory refresh successful.");
                    Inventory inv = null;
                    try {
                        inv = queryInventory(querySkuDetails, moreSkus);
                    }
                    catch (IabException ex) {
                        result = ex.getResult();
                    } catch(MyIllegalStateException ex){ 
    				    result = new IabResult(BILLING_RESPONSE_RESULT_ERROR, ex.getMessage());
    				    ex.printStackTrace();
    				}
    
                    flagEndAsync();
    
                    final IabResult result_f = result;
                    final Inventory inv_f = inv;
                    if (!mDisposed && listener != null) {
                        handler.post(new Runnable() {
                            public void run() {
                                listener.onQueryInventoryFinished(result_f, inv_f);
                            }
                        });
                    }
                }
            })).start();
        }
    
        public void queryInventoryAsync(QueryInventoryFinishedListener listener) throws MyIllegalStateException {
            queryInventoryAsync(true, null, listener);
        }
    
        public void queryInventoryAsync(boolean querySkuDetails, QueryInventoryFinishedListener listener) throws MyIllegalStateException {
            queryInventoryAsync(querySkuDetails, null, listener);
        }
    
    
        /**
         * Consumes a given in-app product. Consuming can only be done on an item
         * that's owned, and as a result of consumption, the user will no longer own it.
         * This method may block or take long to return. Do not call from the UI thread.
         * For that, see {@link #consumeAsync}.
         *
         * @param itemInfo The PurchaseInfo that represents the item to consume.
         * @throws IabException if there is a problem during consumption.
         */
        void consume(Purchase itemInfo) throws IabException {
            checkNotDisposed();
            checkSetupDone("consume");
    
            if (!itemInfo.mItemType.equals(ITEM_TYPE_INAPP)) {
                throw new IabException(IABHELPER_INVALID_CONSUMPTION,
                        "Items of type '" + itemInfo.mItemType + "' can't be consumed.");
            }
    
            try {
                String token = itemInfo.getToken();
                String sku = itemInfo.getSku();
                if (token == null || token.equals("")) {
                   logError("Can't consume "+ sku + ". No token.");
                   throw new IabException(IABHELPER_MISSING_TOKEN, "PurchaseInfo is missing token for sku: "
                       + sku + " " + itemInfo);
                }
    
                logDebug("Consuming sku: " + sku + ", token: " + token);
                int response = mService.consumePurchase(3, mContext.getPackageName(), token);
                if (response == BILLING_RESPONSE_RESULT_OK) {
                   logDebug("Successfully consumed sku: " + sku);
                }
                else {
                   logDebug("Error consuming consuming sku " + sku + ". " + getResponseDesc(response));
                   throw new IabException(response, "Error consuming sku " + sku);
                }
            }
            catch (RemoteException e) {
                throw new IabException(IABHELPER_REMOTE_EXCEPTION, "Remote exception while consuming. PurchaseInfo: " + itemInfo, e);
            }
        }
    
        /**
         * Callback that notifies when a consumption operation finishes.
         */
        public interface OnConsumeFinishedListener {
            /**
             * Called to notify that a consumption has finished.
             *
             * @param purchase The purchase that was (or was to be) consumed.
             * @param result The result of the consumption operation.
             */
            public void onConsumeFinished(Purchase purchase, IabResult result);
        }
    
        /**
         * Callback that notifies when a multi-item consumption operation finishes.
         */
        public interface OnConsumeMultiFinishedListener {
            /**
             * Called to notify that a consumption of multiple items has finished.
             *
             * @param purchases The purchases that were (or were to be) consumed.
             * @param results The results of each consumption operation, corresponding to each
             *     sku.
             */
            public void onConsumeMultiFinished(List purchases, List results);
        }
    
        /**
         * Asynchronous wrapper to item consumption. Works like {@link #consume}, but
         * performs the consumption in the background and notifies completion through
         * the provided listener. This method is safe to call from a UI thread.
         *
         * @param purchase The purchase to be consumed.
         * @param listener The listener to notify when the consumption operation finishes.
         */
        public void consumeAsync(Purchase purchase, OnConsumeFinishedListener listener) throws MyIllegalStateException {
            checkNotDisposed();
            checkSetupDone("consume");
            List purchases = new ArrayList();
            purchases.add(purchase);
            consumeAsyncInternal(purchases, listener, null);
        }
    
        /**
         * Same as {@link consumeAsync}, but for multiple items at once.
         * @param purchases The list of PurchaseInfo objects representing the purchases to consume.
         * @param listener The listener to notify when the consumption operation finishes.
         */
        public void consumeAsync(List purchases, OnConsumeMultiFinishedListener listener) throws MyIllegalStateException {
            checkNotDisposed();
            checkSetupDone("consume");
            consumeAsyncInternal(purchases, null, listener);
        }
    
        /**
         * Returns a human-readable description for the given response code.
         *
         * @param code The response code
         * @return A human-readable string explaining the result code.
         *     It also includes the result code numerically.
         */
        public static String getResponseDesc(int code) {
            String[] iab_msgs = ("0:OK/1:User Canceled/2:Unknown/" +
                    "3:Billing Unavailable/4:Item unavailable/" +
                    "5:Developer Error/6:Error/7:Item Already Owned/" +
                    "8:Item not owned").split("/");
            String[] iabhelper_msgs = ("0:OK/-1001:Remote exception during initialization/" +
                                       "-1002:Bad response received/" +
                                       "-1003:Purchase signature verification failed/" +
                                       "-1004:Send intent failed/" +
                                       "-1005:User cancelled/" +
                                       "-1006:Unknown purchase response/" +
                                       "-1007:Missing token/" +
                                       "-1008:Unknown error/" +
                                       "-1009:Subscriptions not available/" +
                                       "-1010:Invalid consumption attempt").split("/");
    
            if (code <= IABHELPER_ERROR_BASE) {
                int index = IABHELPER_ERROR_BASE - code;
                if (index >= 0 && index < iabhelper_msgs.length) return iabhelper_msgs[index];
                else return String.valueOf(code) + ":Unknown IAB Helper Error";
            }
            else if (code < 0 || code >= iab_msgs.length)
                return String.valueOf(code) + ":Unknown";
            else
                return iab_msgs[code];
        }
    
    
        // Checks that setup was done; if not, throws an exception.
        void checkSetupDone(String operation) throws MyIllegalStateException {
            if (!mSetupDone) {
                logError("Illegal state for operation (" + operation + "): IAB helper is not set up.");
                throw new MyIllegalStateException("IAB helper is not set up. Can't perform operation: " + operation);
            }
        }
    
        // Workaround to bug where sometimes response codes come as Long instead of Integer
        int getResponseCodeFromBundle(Bundle b) {
            Object o = b.get(RESPONSE_CODE);
            if (o == null) {
                logDebug("Bundle with null response code, assuming OK (known issue)");
                return BILLING_RESPONSE_RESULT_OK;
            }
            else if (o instanceof Integer) return ((Integer)o).intValue();
            else if (o instanceof Long) return (int)((Long)o).longValue();
            else {
                logError("Unexpected type for bundle response code.");
                logError(o.getClass().getName());
                throw new RuntimeException("Unexpected type for bundle response code: " + o.getClass().getName());
            }
        }
    
        // Workaround to bug where sometimes response codes come as Long instead of Integer
        int getResponseCodeFromIntent(Intent i) {
            Object o = i.getExtras().get(RESPONSE_CODE);
            if (o == null) {
                logError("Intent with no response code, assuming OK (known issue)");
                return BILLING_RESPONSE_RESULT_OK;
            }
            else if (o instanceof Integer) return ((Integer)o).intValue();
            else if (o instanceof Long) return (int)((Long)o).longValue();
            else {
                logError("Unexpected type for intent response code.");
                logError(o.getClass().getName());
                throw new RuntimeException("Unexpected type for intent response code: " + o.getClass().getName());
            }
        }
    
        void flagStartAsync(String operation) throws MyIllegalStateException {
            if (mAsyncInProgress) throw new MyIllegalStateException("Can't start async operation (" +
                    operation + ") because another async operation(" + mAsyncOperation + ") is in progress.");
            mAsyncOperation = operation;
            mAsyncInProgress = true;
            logDebug("Starting async operation: " + operation);
        }
    
        void flagEndAsync() {
            logDebug("Ending async operation: " + mAsyncOperation);
            mAsyncOperation = "";
            mAsyncInProgress = false;
        }
    
    
        int queryPurchases(Inventory inv, String itemType) throws JSONException, RemoteException {
            // Query purchases
            logDebug("Querying owned items, item type: " + itemType);
            logDebug("Package name: " + mContext.getPackageName());
            boolean verificationFailed = false;
            String continueToken = null;
    
            do {
                logDebug("Calling getPurchases with continuation token: " + continueToken);
                Bundle ownedItems = mService.getPurchases(3, mContext.getPackageName(),
                        itemType, continueToken);
    
                int response = getResponseCodeFromBundle(ownedItems);
                logDebug("Owned items response: " + String.valueOf(response));
                if (response != BILLING_RESPONSE_RESULT_OK) {
                    logDebug("getPurchases() failed: " + getResponseDesc(response));
                    return response;
                }
                if (!ownedItems.containsKey(RESPONSE_INAPP_ITEM_LIST)
                        || !ownedItems.containsKey(RESPONSE_INAPP_PURCHASE_DATA_LIST)
                        || !ownedItems.containsKey(RESPONSE_INAPP_SIGNATURE_LIST)) {
                    logError("Bundle returned from getPurchases() doesn't contain required fields.");
                    return IABHELPER_BAD_RESPONSE;
                }
    
                ArrayList ownedSkus = ownedItems.getStringArrayList(
                            RESPONSE_INAPP_ITEM_LIST);
                ArrayList purchaseDataList = ownedItems.getStringArrayList(
                            RESPONSE_INAPP_PURCHASE_DATA_LIST);
                ArrayList signatureList = ownedItems.getStringArrayList(
                            RESPONSE_INAPP_SIGNATURE_LIST);
    
                for (int i = 0; i < purchaseDataList.size(); ++i) {
                    String purchaseData = purchaseDataList.get(i);
                    String signature = signatureList.get(i);
                    String sku = ownedSkus.get(i);
                    if (Security.verifyPurchase(mSignatureBase64, purchaseData, signature)) {
                        logDebug("Sku is owned: " + sku);
                        Purchase purchase = new Purchase(itemType, purchaseData, signature);
    
                        if (TextUtils.isEmpty(purchase.getToken())) {
                            logWarn("BUG: empty/null token!");
                            logDebug("Purchase data: " + purchaseData);
                        }
    
                        // Record ownership and token
                        inv.addPurchase(purchase);
                    }
                    else {
                        logWarn("Purchase signature verification **FAILED**. Not adding item.");
                        logDebug("   Purchase data: " + purchaseData);
                        logDebug("   Signature: " + signature);
                        verificationFailed = true;
                    }
                }
    
                continueToken = ownedItems.getString(INAPP_CONTINUATION_TOKEN);
                logDebug("Continuation token: " + continueToken);
            } while (!TextUtils.isEmpty(continueToken));
    
            return verificationFailed ? IABHELPER_VERIFICATION_FAILED : BILLING_RESPONSE_RESULT_OK;
        }
    
        int querySkuDetails(String itemType, Inventory inv, List moreSkus)
                                    throws RemoteException, JSONException {
            logDebug("Querying SKU details.");
            ArrayList skuList = new ArrayList();
            skuList.addAll(inv.getAllOwnedSkus(itemType));
            if (moreSkus != null) {
                for (String sku : moreSkus) {
                    if (!skuList.contains(sku)) {
                        skuList.add(sku);
                    }
                }
            }
    
            if (skuList.size() == 0) {
                logDebug("queryPrices: nothing to do because there are no SKUs.");
                return BILLING_RESPONSE_RESULT_OK;
            }
    
            Bundle querySkus = new Bundle();
            querySkus.putStringArrayList(GET_SKU_DETAILS_ITEM_LIST, skuList);
            Bundle skuDetails = mService.getSkuDetails(3, mContext.getPackageName(),
                    itemType, querySkus);
    
            if (!skuDetails.containsKey(RESPONSE_GET_SKU_DETAILS_LIST)) {
                int response = getResponseCodeFromBundle(skuDetails);
                if (response != BILLING_RESPONSE_RESULT_OK) {
                    logDebug("getSkuDetails() failed: " + getResponseDesc(response));
                    return response;
                }
                else {
                    logError("getSkuDetails() returned a bundle with neither an error nor a detail list.");
                    return IABHELPER_BAD_RESPONSE;
                }
            }
    
            ArrayList responseList = skuDetails.getStringArrayList(
                    RESPONSE_GET_SKU_DETAILS_LIST);
    
            for (String thisResponse : responseList) {
                SkuDetails d = new SkuDetails(itemType, thisResponse);
                logDebug("Got sku details: " + d);
                inv.addSkuDetails(d);
            }
            return BILLING_RESPONSE_RESULT_OK;
        }
    
    
        void consumeAsyncInternal(final List purchases,
                                  final OnConsumeFinishedListener singleListener,
                                  final OnConsumeMultiFinishedListener multiListener) throws MyIllegalStateException {
            final Handler handler = new Handler();
            flagStartAsync("consume");
            (new Thread(new Runnable() {
                public void run() {
                    final List results = new ArrayList();
                    for (Purchase purchase : purchases) {
                        try {
                            consume(purchase);
                            results.add(new IabResult(BILLING_RESPONSE_RESULT_OK, "Successful consume of sku " + purchase.getSku()));
                        }
                        catch (IabException ex) {
                            results.add(ex.getResult());
                        }
                    }
    
                    flagEndAsync();
                    if (!mDisposed && singleListener != null) {
                        handler.post(new Runnable() {
                            public void run() {
                                singleListener.onConsumeFinished(purchases.get(0), results.get(0));
                            }
                        });
                    }
                    if (!mDisposed && multiListener != null) {
                        handler.post(new Runnable() {
                            public void run() {
                                multiListener.onConsumeMultiFinished(purchases, results);
                            }
                        });
                    }
                }
            })).start();
        }
    
        void logDebug(String msg) {
            if (mDebugLog) Log.d(mDebugTag, msg);
        }
    
        void logError(String msg) {
            Log.e(mDebugTag, "In-app billing error: " + msg);
        }
    
        void logWarn(String msg) {
            Log.w(mDebugTag, "In-app billing warning: " + msg);
        }
    }