/[projects]/android/TrainInfo/src/com/example/android/trivialdrivesample/util/IabHelper.java
ViewVC logotype

Annotation of /android/TrainInfo/src/com/example/android/trivialdrivesample/util/IabHelper.java

Parent Directory Parent Directory | Revision Log Revision Log


Revision 2123 - (hide annotations) (download)
Wed Mar 5 12:11:16 2014 UTC (10 years, 3 months ago) by torben
File size: 44347 byte(s)
Add billing code
1 torben 2123 /* Copyright (c) 2012 Google Inc.
2     *
3     * Licensed under the Apache License, Version 2.0 (the "License");
4     * you may not use this file except in compliance with the License.
5     * You may obtain a copy of the License at
6     *
7     * http://www.apache.org/licenses/LICENSE-2.0
8     *
9     * Unless required by applicable law or agreed to in writing, software
10     * distributed under the License is distributed on an "AS IS" BASIS,
11     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12     * See the License for the specific language governing permissions and
13     * limitations under the License.
14     */
15    
16     package com.example.android.trivialdrivesample.util;
17    
18     import android.app.Activity;
19     import android.app.PendingIntent;
20     import android.content.ComponentName;
21     import android.content.Context;
22     import android.content.Intent;
23     import android.content.IntentSender.SendIntentException;
24     import android.content.ServiceConnection;
25     import android.os.Bundle;
26     import android.os.Handler;
27     import android.os.IBinder;
28     import android.os.RemoteException;
29     import android.text.TextUtils;
30     import android.util.Log;
31    
32     import com.android.vending.billing.IInAppBillingService;
33    
34     import org.json.JSONException;
35    
36     import java.util.ArrayList;
37     import java.util.List;
38    
39    
40     /**
41     * Provides convenience methods for in-app billing. You can create one instance of this
42     * class for your application and use it to process in-app billing operations.
43     * It provides synchronous (blocking) and asynchronous (non-blocking) methods for
44     * many common in-app billing operations, as well as automatic signature
45     * verification.
46     *
47     * After instantiating, you must perform setup in order to start using the object.
48     * To perform setup, call the {@link #startSetup} method and provide a listener;
49     * that listener will be notified when setup is complete, after which (and not before)
50     * you may call other methods.
51     *
52     * After setup is complete, you will typically want to request an inventory of owned
53     * items and subscriptions. See {@link #queryInventory}, {@link #queryInventoryAsync}
54     * and related methods.
55     *
56     * When you are done with this object, don't forget to call {@link #dispose}
57     * to ensure proper cleanup. This object holds a binding to the in-app billing
58     * service, which will leak unless you dispose of it correctly. If you created
59     * the object on an Activity's onCreate method, then the recommended
60     * place to dispose of it is the Activity's onDestroy method.
61     *
62     * A note about threading: When using this object from a background thread, you may
63     * call the blocking versions of methods; when using from a UI thread, call
64     * only the asynchronous versions and handle the results via callbacks.
65     * Also, notice that you can only call one asynchronous operation at a time;
66     * attempting to start a second asynchronous operation while the first one
67     * has not yet completed will result in an exception being thrown.
68     *
69     * @author Bruno Oliveira (Google)
70     *
71     */
72     public class IabHelper {
73     // Is debug logging enabled?
74     boolean mDebugLog = false;
75     String mDebugTag = "IabHelper";
76    
77     // Is setup done?
78     boolean mSetupDone = false;
79    
80     // Has this object been disposed of? (If so, we should ignore callbacks, etc)
81     boolean mDisposed = false;
82    
83     // Are subscriptions supported?
84     boolean mSubscriptionsSupported = false;
85    
86     // Is an asynchronous operation in progress?
87     // (only one at a time can be in progress)
88     boolean mAsyncInProgress = false;
89    
90     // (for logging/debugging)
91     // if mAsyncInProgress == true, what asynchronous operation is in progress?
92     String mAsyncOperation = "";
93    
94     // Context we were passed during initialization
95     Context mContext;
96    
97     // Connection to the service
98     IInAppBillingService mService;
99     ServiceConnection mServiceConn;
100    
101     // The request code used to launch purchase flow
102     int mRequestCode;
103    
104     // The item type of the current purchase flow
105     String mPurchasingItemType;
106    
107     // Public key for verifying signature, in base64 encoding
108     String mSignatureBase64 = null;
109    
110     // Billing response codes
111     public static final int BILLING_RESPONSE_RESULT_OK = 0;
112     public static final int BILLING_RESPONSE_RESULT_USER_CANCELED = 1;
113     public static final int BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE = 3;
114     public static final int BILLING_RESPONSE_RESULT_ITEM_UNAVAILABLE = 4;
115     public static final int BILLING_RESPONSE_RESULT_DEVELOPER_ERROR = 5;
116     public static final int BILLING_RESPONSE_RESULT_ERROR = 6;
117     public static final int BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED = 7;
118     public static final int BILLING_RESPONSE_RESULT_ITEM_NOT_OWNED = 8;
119    
120     // IAB Helper error codes
121     public static final int IABHELPER_ERROR_BASE = -1000;
122     public static final int IABHELPER_REMOTE_EXCEPTION = -1001;
123     public static final int IABHELPER_BAD_RESPONSE = -1002;
124     public static final int IABHELPER_VERIFICATION_FAILED = -1003;
125     public static final int IABHELPER_SEND_INTENT_FAILED = -1004;
126     public static final int IABHELPER_USER_CANCELLED = -1005;
127     public static final int IABHELPER_UNKNOWN_PURCHASE_RESPONSE = -1006;
128     public static final int IABHELPER_MISSING_TOKEN = -1007;
129     public static final int IABHELPER_UNKNOWN_ERROR = -1008;
130     public static final int IABHELPER_SUBSCRIPTIONS_NOT_AVAILABLE = -1009;
131     public static final int IABHELPER_INVALID_CONSUMPTION = -1010;
132    
133     // Keys for the responses from InAppBillingService
134     public static final String RESPONSE_CODE = "RESPONSE_CODE";
135     public static final String RESPONSE_GET_SKU_DETAILS_LIST = "DETAILS_LIST";
136     public static final String RESPONSE_BUY_INTENT = "BUY_INTENT";
137     public static final String RESPONSE_INAPP_PURCHASE_DATA = "INAPP_PURCHASE_DATA";
138     public static final String RESPONSE_INAPP_SIGNATURE = "INAPP_DATA_SIGNATURE";
139     public static final String RESPONSE_INAPP_ITEM_LIST = "INAPP_PURCHASE_ITEM_LIST";
140     public static final String RESPONSE_INAPP_PURCHASE_DATA_LIST = "INAPP_PURCHASE_DATA_LIST";
141     public static final String RESPONSE_INAPP_SIGNATURE_LIST = "INAPP_DATA_SIGNATURE_LIST";
142     public static final String INAPP_CONTINUATION_TOKEN = "INAPP_CONTINUATION_TOKEN";
143    
144     // Item types
145     public static final String ITEM_TYPE_INAPP = "inapp";
146     public static final String ITEM_TYPE_SUBS = "subs";
147    
148     // some fields on the getSkuDetails response bundle
149     public static final String GET_SKU_DETAILS_ITEM_LIST = "ITEM_ID_LIST";
150     public static final String GET_SKU_DETAILS_ITEM_TYPE_LIST = "ITEM_TYPE_LIST";
151    
152     /**
153     * Creates an instance. After creation, it will not yet be ready to use. You must perform
154     * setup by calling {@link #startSetup} and wait for setup to complete. This constructor does not
155     * block and is safe to call from a UI thread.
156     *
157     * @param ctx Your application or Activity context. Needed to bind to the in-app billing service.
158     * @param base64PublicKey Your application's public key, encoded in base64.
159     * This is used for verification of purchase signatures. You can find your app's base64-encoded
160     * public key in your application's page on Google Play Developer Console. Note that this
161     * is NOT your "developer public key".
162     */
163     public IabHelper(Context ctx, String base64PublicKey) {
164     mContext = ctx.getApplicationContext();
165     mSignatureBase64 = base64PublicKey;
166     logDebug("IAB helper created.");
167     }
168    
169     /**
170     * Enables or disable debug logging through LogCat.
171     */
172     public void enableDebugLogging(boolean enable, String tag) {
173     checkNotDisposed();
174     mDebugLog = enable;
175     mDebugTag = tag;
176     }
177    
178     public void enableDebugLogging(boolean enable) {
179     checkNotDisposed();
180     mDebugLog = enable;
181     }
182    
183     /**
184     * Callback for setup process. This listener's {@link #onIabSetupFinished} method is called
185     * when the setup process is complete.
186     */
187     public interface OnIabSetupFinishedListener {
188     /**
189     * Called to notify that setup is complete.
190     *
191     * @param result The result of the setup process.
192     */
193     public void onIabSetupFinished(IabResult result);
194     }
195    
196     /**
197     * Starts the setup process. This will start up the setup process asynchronously.
198     * You will be notified through the listener when the setup process is complete.
199     * This method is safe to call from a UI thread.
200     *
201     * @param listener The listener to notify when the setup process is complete.
202     */
203     public void startSetup(final OnIabSetupFinishedListener listener) {
204     // If already set up, can't do it again.
205     checkNotDisposed();
206     if (mSetupDone) throw new IllegalStateException("IAB helper is already set up.");
207    
208     // Connection to IAB service
209     logDebug("Starting in-app billing setup.");
210     mServiceConn = new ServiceConnection() {
211     @Override
212     public void onServiceDisconnected(ComponentName name) {
213     logDebug("Billing service disconnected.");
214     mService = null;
215     }
216    
217     @Override
218     public void onServiceConnected(ComponentName name, IBinder service) {
219     if (mDisposed) return;
220     logDebug("Billing service connected.");
221     mService = IInAppBillingService.Stub.asInterface(service);
222     String packageName = mContext.getPackageName();
223     try {
224     logDebug("Checking for in-app billing 3 support.");
225    
226     // check for in-app billing v3 support
227     int response = mService.isBillingSupported(3, packageName, ITEM_TYPE_INAPP);
228     if (response != BILLING_RESPONSE_RESULT_OK) {
229     if (listener != null) listener.onIabSetupFinished(new IabResult(response,
230     "Error checking for billing v3 support."));
231    
232     // if in-app purchases aren't supported, neither are subscriptions.
233     mSubscriptionsSupported = false;
234     return;
235     }
236     logDebug("In-app billing version 3 supported for " + packageName);
237    
238     // check for v3 subscriptions support
239     response = mService.isBillingSupported(3, packageName, ITEM_TYPE_SUBS);
240     if (response == BILLING_RESPONSE_RESULT_OK) {
241     logDebug("Subscriptions AVAILABLE.");
242     mSubscriptionsSupported = true;
243     }
244     else {
245     logDebug("Subscriptions NOT AVAILABLE. Response: " + response);
246     }
247    
248     mSetupDone = true;
249     }
250     catch (RemoteException e) {
251     if (listener != null) {
252     listener.onIabSetupFinished(new IabResult(IABHELPER_REMOTE_EXCEPTION,
253     "RemoteException while setting up in-app billing."));
254     }
255     e.printStackTrace();
256     return;
257     }
258    
259     if (listener != null) {
260     listener.onIabSetupFinished(new IabResult(BILLING_RESPONSE_RESULT_OK, "Setup successful."));
261     }
262     }
263     };
264    
265     Intent serviceIntent = new Intent("com.android.vending.billing.InAppBillingService.BIND");
266     serviceIntent.setPackage("com.android.vending");
267     if (!mContext.getPackageManager().queryIntentServices(serviceIntent, 0).isEmpty()) {
268     // service available to handle that Intent
269     mContext.bindService(serviceIntent, mServiceConn, Context.BIND_AUTO_CREATE);
270     }
271     else {
272     // no service available to handle that Intent
273     if (listener != null) {
274     listener.onIabSetupFinished(
275     new IabResult(BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE,
276     "Billing service unavailable on device."));
277     }
278     }
279     }
280    
281     /**
282     * Dispose of object, releasing resources. It's very important to call this
283     * method when you are done with this object. It will release any resources
284     * used by it such as service connections. Naturally, once the object is
285     * disposed of, it can't be used again.
286     */
287     public void dispose() {
288     logDebug("Disposing.");
289     mSetupDone = false;
290     if (mServiceConn != null) {
291     logDebug("Unbinding from service.");
292     if (mContext != null) mContext.unbindService(mServiceConn);
293     }
294     mDisposed = true;
295     mContext = null;
296     mServiceConn = null;
297     mService = null;
298     mPurchaseListener = null;
299     }
300    
301     private void checkNotDisposed() {
302     if (mDisposed) throw new IllegalStateException("IabHelper was disposed of, so it cannot be used.");
303     }
304    
305     /** Returns whether subscriptions are supported. */
306     public boolean subscriptionsSupported() {
307     checkNotDisposed();
308     return mSubscriptionsSupported;
309     }
310    
311    
312     /**
313     * Callback that notifies when a purchase is finished.
314     */
315     public interface OnIabPurchaseFinishedListener {
316     /**
317     * Called to notify that an in-app purchase finished. If the purchase was successful,
318     * then the sku parameter specifies which item was purchased. If the purchase failed,
319     * the sku and extraData parameters may or may not be null, depending on how far the purchase
320     * process went.
321     *
322     * @param result The result of the purchase.
323     * @param info The purchase information (null if purchase failed)
324     */
325     public void onIabPurchaseFinished(IabResult result, Purchase info);
326     }
327    
328     // The listener registered on launchPurchaseFlow, which we have to call back when
329     // the purchase finishes
330     OnIabPurchaseFinishedListener mPurchaseListener;
331    
332     public void launchPurchaseFlow(Activity act, String sku, int requestCode, OnIabPurchaseFinishedListener listener) {
333     launchPurchaseFlow(act, sku, requestCode, listener, "");
334     }
335    
336     public void launchPurchaseFlow(Activity act, String sku, int requestCode,
337     OnIabPurchaseFinishedListener listener, String extraData) {
338     launchPurchaseFlow(act, sku, ITEM_TYPE_INAPP, requestCode, listener, extraData);
339     }
340    
341     public void launchSubscriptionPurchaseFlow(Activity act, String sku, int requestCode,
342     OnIabPurchaseFinishedListener listener) {
343     launchSubscriptionPurchaseFlow(act, sku, requestCode, listener, "");
344     }
345    
346     public void launchSubscriptionPurchaseFlow(Activity act, String sku, int requestCode,
347     OnIabPurchaseFinishedListener listener, String extraData) {
348     launchPurchaseFlow(act, sku, ITEM_TYPE_SUBS, requestCode, listener, extraData);
349     }
350    
351     /**
352     * Initiate the UI flow for an in-app purchase. Call this method to initiate an in-app purchase,
353     * which will involve bringing up the Google Play screen. The calling activity will be paused while
354     * the user interacts with Google Play, and the result will be delivered via the activity's
355     * {@link android.app.Activity#onActivityResult} method, at which point you must call
356     * this object's {@link #handleActivityResult} method to continue the purchase flow. This method
357     * MUST be called from the UI thread of the Activity.
358     *
359     * @param act The calling activity.
360     * @param sku The sku of the item to purchase.
361     * @param itemType indicates if it's a product or a subscription (ITEM_TYPE_INAPP or ITEM_TYPE_SUBS)
362     * @param requestCode A request code (to differentiate from other responses --
363     * as in {@link android.app.Activity#startActivityForResult}).
364     * @param listener The listener to notify when the purchase process finishes
365     * @param extraData Extra data (developer payload), which will be returned with the purchase data
366     * when the purchase completes. This extra data will be permanently bound to that purchase
367     * and will always be returned when the purchase is queried.
368     */
369     public void launchPurchaseFlow(Activity act, String sku, String itemType, int requestCode,
370     OnIabPurchaseFinishedListener listener, String extraData) {
371     checkNotDisposed();
372     checkSetupDone("launchPurchaseFlow");
373     flagStartAsync("launchPurchaseFlow");
374     IabResult result;
375    
376     if (itemType.equals(ITEM_TYPE_SUBS) && !mSubscriptionsSupported) {
377     IabResult r = new IabResult(IABHELPER_SUBSCRIPTIONS_NOT_AVAILABLE,
378     "Subscriptions are not available.");
379     flagEndAsync();
380     if (listener != null) listener.onIabPurchaseFinished(r, null);
381     return;
382     }
383    
384     try {
385     logDebug("Constructing buy intent for " + sku + ", item type: " + itemType);
386     Bundle buyIntentBundle = mService.getBuyIntent(3, mContext.getPackageName(), sku, itemType, extraData);
387     int response = getResponseCodeFromBundle(buyIntentBundle);
388     if (response != BILLING_RESPONSE_RESULT_OK) {
389     logError("Unable to buy item, Error response: " + getResponseDesc(response));
390     flagEndAsync();
391     result = new IabResult(response, "Unable to buy item");
392     if (listener != null) listener.onIabPurchaseFinished(result, null);
393     return;
394     }
395    
396     PendingIntent pendingIntent = buyIntentBundle.getParcelable(RESPONSE_BUY_INTENT);
397     logDebug("Launching buy intent for " + sku + ". Request code: " + requestCode);
398     mRequestCode = requestCode;
399     mPurchaseListener = listener;
400     mPurchasingItemType = itemType;
401     act.startIntentSenderForResult(pendingIntent.getIntentSender(),
402     requestCode, new Intent(),
403     Integer.valueOf(0), Integer.valueOf(0),
404     Integer.valueOf(0));
405     }
406     catch (SendIntentException e) {
407     logError("SendIntentException while launching purchase flow for sku " + sku);
408     e.printStackTrace();
409     flagEndAsync();
410    
411     result = new IabResult(IABHELPER_SEND_INTENT_FAILED, "Failed to send intent.");
412     if (listener != null) listener.onIabPurchaseFinished(result, null);
413     }
414     catch (RemoteException e) {
415     logError("RemoteException while launching purchase flow for sku " + sku);
416     e.printStackTrace();
417     flagEndAsync();
418    
419     result = new IabResult(IABHELPER_REMOTE_EXCEPTION, "Remote exception while starting purchase flow");
420     if (listener != null) listener.onIabPurchaseFinished(result, null);
421     }
422     }
423    
424     /**
425     * Handles an activity result that's part of the purchase flow in in-app billing. If you
426     * are calling {@link #launchPurchaseFlow}, then you must call this method from your
427     * Activity's {@link android.app.Activity@onActivityResult} method. This method
428     * MUST be called from the UI thread of the Activity.
429     *
430     * @param requestCode The requestCode as you received it.
431     * @param resultCode The resultCode as you received it.
432     * @param data The data (Intent) as you received it.
433     * @return Returns true if the result was related to a purchase flow and was handled;
434     * false if the result was not related to a purchase, in which case you should
435     * handle it normally.
436     */
437     public boolean handleActivityResult(int requestCode, int resultCode, Intent data) {
438     IabResult result;
439     if (requestCode != mRequestCode) return false;
440    
441     checkNotDisposed();
442     checkSetupDone("handleActivityResult");
443    
444     // end of async purchase operation that started on launchPurchaseFlow
445     flagEndAsync();
446    
447     if (data == null) {
448     logError("Null data in IAB activity result.");
449     result = new IabResult(IABHELPER_BAD_RESPONSE, "Null data in IAB result");
450     if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
451     return true;
452     }
453    
454     int responseCode = getResponseCodeFromIntent(data);
455     String purchaseData = data.getStringExtra(RESPONSE_INAPP_PURCHASE_DATA);
456     String dataSignature = data.getStringExtra(RESPONSE_INAPP_SIGNATURE);
457    
458     if (resultCode == Activity.RESULT_OK && responseCode == BILLING_RESPONSE_RESULT_OK) {
459     logDebug("Successful resultcode from purchase activity.");
460     logDebug("Purchase data: " + purchaseData);
461     logDebug("Data signature: " + dataSignature);
462     logDebug("Extras: " + data.getExtras());
463     logDebug("Expected item type: " + mPurchasingItemType);
464    
465     if (purchaseData == null || dataSignature == null) {
466     logError("BUG: either purchaseData or dataSignature is null.");
467     logDebug("Extras: " + data.getExtras().toString());
468     result = new IabResult(IABHELPER_UNKNOWN_ERROR, "IAB returned null purchaseData or dataSignature");
469     if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
470     return true;
471     }
472    
473     Purchase purchase = null;
474     try {
475     purchase = new Purchase(mPurchasingItemType, purchaseData, dataSignature);
476     String sku = purchase.getSku();
477    
478     // Verify signature
479     if (!Security.verifyPurchase(mSignatureBase64, purchaseData, dataSignature)) {
480     logError("Purchase signature verification FAILED for sku " + sku);
481     result = new IabResult(IABHELPER_VERIFICATION_FAILED, "Signature verification failed for sku " + sku);
482     if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, purchase);
483     return true;
484     }
485     logDebug("Purchase signature successfully verified.");
486     }
487     catch (JSONException e) {
488     logError("Failed to parse purchase data.");
489     e.printStackTrace();
490     result = new IabResult(IABHELPER_BAD_RESPONSE, "Failed to parse purchase data.");
491     if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
492     return true;
493     }
494    
495     if (mPurchaseListener != null) {
496     mPurchaseListener.onIabPurchaseFinished(new IabResult(BILLING_RESPONSE_RESULT_OK, "Success"), purchase);
497     }
498     }
499     else if (resultCode == Activity.RESULT_OK) {
500     // result code was OK, but in-app billing response was not OK.
501     logDebug("Result code was OK but in-app billing response was not OK: " + getResponseDesc(responseCode));
502     if (mPurchaseListener != null) {
503     result = new IabResult(responseCode, "Problem purchashing item.");
504     mPurchaseListener.onIabPurchaseFinished(result, null);
505     }
506     }
507     else if (resultCode == Activity.RESULT_CANCELED) {
508     logDebug("Purchase canceled - Response: " + getResponseDesc(responseCode));
509     result = new IabResult(IABHELPER_USER_CANCELLED, "User canceled.");
510     if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
511     }
512     else {
513     logError("Purchase failed. Result code: " + Integer.toString(resultCode)
514     + ". Response: " + getResponseDesc(responseCode));
515     result = new IabResult(IABHELPER_UNKNOWN_PURCHASE_RESPONSE, "Unknown purchase response.");
516     if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
517     }
518     return true;
519     }
520    
521     public Inventory queryInventory(boolean querySkuDetails, List<String> moreSkus) throws IabException {
522     return queryInventory(querySkuDetails, moreSkus, null);
523     }
524    
525     /**
526     * Queries the inventory. This will query all owned items from the server, as well as
527     * information on additional skus, if specified. This method may block or take long to execute.
528     * Do not call from a UI thread. For that, use the non-blocking version {@link #refreshInventoryAsync}.
529     *
530     * @param querySkuDetails if true, SKU details (price, description, etc) will be queried as well
531     * as purchase information.
532     * @param moreItemSkus additional PRODUCT skus to query information on, regardless of ownership.
533     * Ignored if null or if querySkuDetails is false.
534     * @param moreSubsSkus additional SUBSCRIPTIONS skus to query information on, regardless of ownership.
535     * Ignored if null or if querySkuDetails is false.
536     * @throws IabException if a problem occurs while refreshing the inventory.
537     */
538     public Inventory queryInventory(boolean querySkuDetails, List<String> moreItemSkus,
539     List<String> moreSubsSkus) throws IabException {
540     checkNotDisposed();
541     checkSetupDone("queryInventory");
542     try {
543     Inventory inv = new Inventory();
544     int r = queryPurchases(inv, ITEM_TYPE_INAPP);
545     if (r != BILLING_RESPONSE_RESULT_OK) {
546     throw new IabException(r, "Error refreshing inventory (querying owned items).");
547     }
548    
549     if (querySkuDetails) {
550     r = querySkuDetails(ITEM_TYPE_INAPP, inv, moreItemSkus);
551     if (r != BILLING_RESPONSE_RESULT_OK) {
552     throw new IabException(r, "Error refreshing inventory (querying prices of items).");
553     }
554     }
555    
556     // if subscriptions are supported, then also query for subscriptions
557     if (mSubscriptionsSupported) {
558     r = queryPurchases(inv, ITEM_TYPE_SUBS);
559     if (r != BILLING_RESPONSE_RESULT_OK) {
560     throw new IabException(r, "Error refreshing inventory (querying owned subscriptions).");
561     }
562    
563     if (querySkuDetails) {
564     r = querySkuDetails(ITEM_TYPE_SUBS, inv, moreItemSkus);
565     if (r != BILLING_RESPONSE_RESULT_OK) {
566     throw new IabException(r, "Error refreshing inventory (querying prices of subscriptions).");
567     }
568     }
569     }
570    
571     return inv;
572     }
573     catch (RemoteException e) {
574     throw new IabException(IABHELPER_REMOTE_EXCEPTION, "Remote exception while refreshing inventory.", e);
575     }
576     catch (JSONException e) {
577     throw new IabException(IABHELPER_BAD_RESPONSE, "Error parsing JSON response while refreshing inventory.", e);
578     }
579     }
580    
581     /**
582     * Listener that notifies when an inventory query operation completes.
583     */
584     public interface QueryInventoryFinishedListener {
585     /**
586     * Called to notify that an inventory query operation completed.
587     *
588     * @param result The result of the operation.
589     * @param inv The inventory.
590     */
591     public void onQueryInventoryFinished(IabResult result, Inventory inv);
592     }
593    
594    
595     /**
596     * Asynchronous wrapper for inventory query. This will perform an inventory
597     * query as described in {@link #queryInventory}, but will do so asynchronously
598     * and call back the specified listener upon completion. This method is safe to
599     * call from a UI thread.
600     *
601     * @param querySkuDetails as in {@link #queryInventory}
602     * @param moreSkus as in {@link #queryInventory}
603     * @param listener The listener to notify when the refresh operation completes.
604     */
605     public void queryInventoryAsync(final boolean querySkuDetails,
606     final List<String> moreSkus,
607     final QueryInventoryFinishedListener listener) {
608     final Handler handler = new Handler();
609     checkNotDisposed();
610     checkSetupDone("queryInventory");
611     flagStartAsync("refresh inventory");
612     (new Thread(new Runnable() {
613     public void run() {
614     IabResult result = new IabResult(BILLING_RESPONSE_RESULT_OK, "Inventory refresh successful.");
615     Inventory inv = null;
616     try {
617     inv = queryInventory(querySkuDetails, moreSkus);
618     }
619     catch (IabException ex) {
620     result = ex.getResult();
621     }
622    
623     flagEndAsync();
624    
625     final IabResult result_f = result;
626     final Inventory inv_f = inv;
627     if (!mDisposed && listener != null) {
628     handler.post(new Runnable() {
629     public void run() {
630     listener.onQueryInventoryFinished(result_f, inv_f);
631     }
632     });
633     }
634     }
635     })).start();
636     }
637    
638     public void queryInventoryAsync(QueryInventoryFinishedListener listener) {
639     queryInventoryAsync(true, null, listener);
640     }
641    
642     public void queryInventoryAsync(boolean querySkuDetails, QueryInventoryFinishedListener listener) {
643     queryInventoryAsync(querySkuDetails, null, listener);
644     }
645    
646    
647     /**
648     * Consumes a given in-app product. Consuming can only be done on an item
649     * that's owned, and as a result of consumption, the user will no longer own it.
650     * This method may block or take long to return. Do not call from the UI thread.
651     * For that, see {@link #consumeAsync}.
652     *
653     * @param itemInfo The PurchaseInfo that represents the item to consume.
654     * @throws IabException if there is a problem during consumption.
655     */
656     void consume(Purchase itemInfo) throws IabException {
657     checkNotDisposed();
658     checkSetupDone("consume");
659    
660     if (!itemInfo.mItemType.equals(ITEM_TYPE_INAPP)) {
661     throw new IabException(IABHELPER_INVALID_CONSUMPTION,
662     "Items of type '" + itemInfo.mItemType + "' can't be consumed.");
663     }
664    
665     try {
666     String token = itemInfo.getToken();
667     String sku = itemInfo.getSku();
668     if (token == null || token.equals("")) {
669     logError("Can't consume "+ sku + ". No token.");
670     throw new IabException(IABHELPER_MISSING_TOKEN, "PurchaseInfo is missing token for sku: "
671     + sku + " " + itemInfo);
672     }
673    
674     logDebug("Consuming sku: " + sku + ", token: " + token);
675     int response = mService.consumePurchase(3, mContext.getPackageName(), token);
676     if (response == BILLING_RESPONSE_RESULT_OK) {
677     logDebug("Successfully consumed sku: " + sku);
678     }
679     else {
680     logDebug("Error consuming consuming sku " + sku + ". " + getResponseDesc(response));
681     throw new IabException(response, "Error consuming sku " + sku);
682     }
683     }
684     catch (RemoteException e) {
685     throw new IabException(IABHELPER_REMOTE_EXCEPTION, "Remote exception while consuming. PurchaseInfo: " + itemInfo, e);
686     }
687     }
688    
689     /**
690     * Callback that notifies when a consumption operation finishes.
691     */
692     public interface OnConsumeFinishedListener {
693     /**
694     * Called to notify that a consumption has finished.
695     *
696     * @param purchase The purchase that was (or was to be) consumed.
697     * @param result The result of the consumption operation.
698     */
699     public void onConsumeFinished(Purchase purchase, IabResult result);
700     }
701    
702     /**
703     * Callback that notifies when a multi-item consumption operation finishes.
704     */
705     public interface OnConsumeMultiFinishedListener {
706     /**
707     * Called to notify that a consumption of multiple items has finished.
708     *
709     * @param purchases The purchases that were (or were to be) consumed.
710     * @param results The results of each consumption operation, corresponding to each
711     * sku.
712     */
713     public void onConsumeMultiFinished(List<Purchase> purchases, List<IabResult> results);
714     }
715    
716     /**
717     * Asynchronous wrapper to item consumption. Works like {@link #consume}, but
718     * performs the consumption in the background and notifies completion through
719     * the provided listener. This method is safe to call from a UI thread.
720     *
721     * @param purchase The purchase to be consumed.
722     * @param listener The listener to notify when the consumption operation finishes.
723     */
724     public void consumeAsync(Purchase purchase, OnConsumeFinishedListener listener) {
725     checkNotDisposed();
726     checkSetupDone("consume");
727     List<Purchase> purchases = new ArrayList<Purchase>();
728     purchases.add(purchase);
729     consumeAsyncInternal(purchases, listener, null);
730     }
731    
732     /**
733     * Same as {@link consumeAsync}, but for multiple items at once.
734     * @param purchases The list of PurchaseInfo objects representing the purchases to consume.
735     * @param listener The listener to notify when the consumption operation finishes.
736     */
737     public void consumeAsync(List<Purchase> purchases, OnConsumeMultiFinishedListener listener) {
738     checkNotDisposed();
739     checkSetupDone("consume");
740     consumeAsyncInternal(purchases, null, listener);
741     }
742    
743     /**
744     * Returns a human-readable description for the given response code.
745     *
746     * @param code The response code
747     * @return A human-readable string explaining the result code.
748     * It also includes the result code numerically.
749     */
750     public static String getResponseDesc(int code) {
751     String[] iab_msgs = ("0:OK/1:User Canceled/2:Unknown/" +
752     "3:Billing Unavailable/4:Item unavailable/" +
753     "5:Developer Error/6:Error/7:Item Already Owned/" +
754     "8:Item not owned").split("/");
755     String[] iabhelper_msgs = ("0:OK/-1001:Remote exception during initialization/" +
756     "-1002:Bad response received/" +
757     "-1003:Purchase signature verification failed/" +
758     "-1004:Send intent failed/" +
759     "-1005:User cancelled/" +
760     "-1006:Unknown purchase response/" +
761     "-1007:Missing token/" +
762     "-1008:Unknown error/" +
763     "-1009:Subscriptions not available/" +
764     "-1010:Invalid consumption attempt").split("/");
765    
766     if (code <= IABHELPER_ERROR_BASE) {
767     int index = IABHELPER_ERROR_BASE - code;
768     if (index >= 0 && index < iabhelper_msgs.length) return iabhelper_msgs[index];
769     else return String.valueOf(code) + ":Unknown IAB Helper Error";
770     }
771     else if (code < 0 || code >= iab_msgs.length)
772     return String.valueOf(code) + ":Unknown";
773     else
774     return iab_msgs[code];
775     }
776    
777    
778     // Checks that setup was done; if not, throws an exception.
779     void checkSetupDone(String operation) {
780     if (!mSetupDone) {
781     logError("Illegal state for operation (" + operation + "): IAB helper is not set up.");
782     throw new IllegalStateException("IAB helper is not set up. Can't perform operation: " + operation);
783     }
784     }
785    
786     // Workaround to bug where sometimes response codes come as Long instead of Integer
787     int getResponseCodeFromBundle(Bundle b) {
788     Object o = b.get(RESPONSE_CODE);
789     if (o == null) {
790     logDebug("Bundle with null response code, assuming OK (known issue)");
791     return BILLING_RESPONSE_RESULT_OK;
792     }
793     else if (o instanceof Integer) return ((Integer)o).intValue();
794     else if (o instanceof Long) return (int)((Long)o).longValue();
795     else {
796     logError("Unexpected type for bundle response code.");
797     logError(o.getClass().getName());
798     throw new RuntimeException("Unexpected type for bundle response code: " + o.getClass().getName());
799     }
800     }
801    
802     // Workaround to bug where sometimes response codes come as Long instead of Integer
803     int getResponseCodeFromIntent(Intent i) {
804     Object o = i.getExtras().get(RESPONSE_CODE);
805     if (o == null) {
806     logError("Intent with no response code, assuming OK (known issue)");
807     return BILLING_RESPONSE_RESULT_OK;
808     }
809     else if (o instanceof Integer) return ((Integer)o).intValue();
810     else if (o instanceof Long) return (int)((Long)o).longValue();
811     else {
812     logError("Unexpected type for intent response code.");
813     logError(o.getClass().getName());
814     throw new RuntimeException("Unexpected type for intent response code: " + o.getClass().getName());
815     }
816     }
817    
818     void flagStartAsync(String operation) {
819     if (mAsyncInProgress) throw new IllegalStateException("Can't start async operation (" +
820     operation + ") because another async operation(" + mAsyncOperation + ") is in progress.");
821     mAsyncOperation = operation;
822     mAsyncInProgress = true;
823     logDebug("Starting async operation: " + operation);
824     }
825    
826     void flagEndAsync() {
827     logDebug("Ending async operation: " + mAsyncOperation);
828     mAsyncOperation = "";
829     mAsyncInProgress = false;
830     }
831    
832    
833     int queryPurchases(Inventory inv, String itemType) throws JSONException, RemoteException {
834     // Query purchases
835     logDebug("Querying owned items, item type: " + itemType);
836     logDebug("Package name: " + mContext.getPackageName());
837     boolean verificationFailed = false;
838     String continueToken = null;
839    
840     do {
841     logDebug("Calling getPurchases with continuation token: " + continueToken);
842     Bundle ownedItems = mService.getPurchases(3, mContext.getPackageName(),
843     itemType, continueToken);
844    
845     int response = getResponseCodeFromBundle(ownedItems);
846     logDebug("Owned items response: " + String.valueOf(response));
847     if (response != BILLING_RESPONSE_RESULT_OK) {
848     logDebug("getPurchases() failed: " + getResponseDesc(response));
849     return response;
850     }
851     if (!ownedItems.containsKey(RESPONSE_INAPP_ITEM_LIST)
852     || !ownedItems.containsKey(RESPONSE_INAPP_PURCHASE_DATA_LIST)
853     || !ownedItems.containsKey(RESPONSE_INAPP_SIGNATURE_LIST)) {
854     logError("Bundle returned from getPurchases() doesn't contain required fields.");
855     return IABHELPER_BAD_RESPONSE;
856     }
857    
858     ArrayList<String> ownedSkus = ownedItems.getStringArrayList(
859     RESPONSE_INAPP_ITEM_LIST);
860     ArrayList<String> purchaseDataList = ownedItems.getStringArrayList(
861     RESPONSE_INAPP_PURCHASE_DATA_LIST);
862     ArrayList<String> signatureList = ownedItems.getStringArrayList(
863     RESPONSE_INAPP_SIGNATURE_LIST);
864    
865     for (int i = 0; i < purchaseDataList.size(); ++i) {
866     String purchaseData = purchaseDataList.get(i);
867     String signature = signatureList.get(i);
868     String sku = ownedSkus.get(i);
869     if (Security.verifyPurchase(mSignatureBase64, purchaseData, signature)) {
870     logDebug("Sku is owned: " + sku);
871     Purchase purchase = new Purchase(itemType, purchaseData, signature);
872    
873     if (TextUtils.isEmpty(purchase.getToken())) {
874     logWarn("BUG: empty/null token!");
875     logDebug("Purchase data: " + purchaseData);
876     }
877    
878     // Record ownership and token
879     inv.addPurchase(purchase);
880     }
881     else {
882     logWarn("Purchase signature verification **FAILED**. Not adding item.");
883     logDebug(" Purchase data: " + purchaseData);
884     logDebug(" Signature: " + signature);
885     verificationFailed = true;
886     }
887     }
888    
889     continueToken = ownedItems.getString(INAPP_CONTINUATION_TOKEN);
890     logDebug("Continuation token: " + continueToken);
891     } while (!TextUtils.isEmpty(continueToken));
892    
893     return verificationFailed ? IABHELPER_VERIFICATION_FAILED : BILLING_RESPONSE_RESULT_OK;
894     }
895    
896     int querySkuDetails(String itemType, Inventory inv, List<String> moreSkus)
897     throws RemoteException, JSONException {
898     logDebug("Querying SKU details.");
899     ArrayList<String> skuList = new ArrayList<String>();
900     skuList.addAll(inv.getAllOwnedSkus(itemType));
901     if (moreSkus != null) {
902     for (String sku : moreSkus) {
903     if (!skuList.contains(sku)) {
904     skuList.add(sku);
905     }
906     }
907     }
908    
909     if (skuList.size() == 0) {
910     logDebug("queryPrices: nothing to do because there are no SKUs.");
911     return BILLING_RESPONSE_RESULT_OK;
912     }
913    
914     Bundle querySkus = new Bundle();
915     querySkus.putStringArrayList(GET_SKU_DETAILS_ITEM_LIST, skuList);
916     Bundle skuDetails = mService.getSkuDetails(3, mContext.getPackageName(),
917     itemType, querySkus);
918    
919     if (!skuDetails.containsKey(RESPONSE_GET_SKU_DETAILS_LIST)) {
920     int response = getResponseCodeFromBundle(skuDetails);
921     if (response != BILLING_RESPONSE_RESULT_OK) {
922     logDebug("getSkuDetails() failed: " + getResponseDesc(response));
923     return response;
924     }
925     else {
926     logError("getSkuDetails() returned a bundle with neither an error nor a detail list.");
927     return IABHELPER_BAD_RESPONSE;
928     }
929     }
930    
931     ArrayList<String> responseList = skuDetails.getStringArrayList(
932     RESPONSE_GET_SKU_DETAILS_LIST);
933    
934     for (String thisResponse : responseList) {
935     SkuDetails d = new SkuDetails(itemType, thisResponse);
936     logDebug("Got sku details: " + d);
937     inv.addSkuDetails(d);
938     }
939     return BILLING_RESPONSE_RESULT_OK;
940     }
941    
942    
943     void consumeAsyncInternal(final List<Purchase> purchases,
944     final OnConsumeFinishedListener singleListener,
945     final OnConsumeMultiFinishedListener multiListener) {
946     final Handler handler = new Handler();
947     flagStartAsync("consume");
948     (new Thread(new Runnable() {
949     public void run() {
950     final List<IabResult> results = new ArrayList<IabResult>();
951     for (Purchase purchase : purchases) {
952     try {
953     consume(purchase);
954     results.add(new IabResult(BILLING_RESPONSE_RESULT_OK, "Successful consume of sku " + purchase.getSku()));
955     }
956     catch (IabException ex) {
957     results.add(ex.getResult());
958     }
959     }
960    
961     flagEndAsync();
962     if (!mDisposed && singleListener != null) {
963     handler.post(new Runnable() {
964     public void run() {
965     singleListener.onConsumeFinished(purchases.get(0), results.get(0));
966     }
967     });
968     }
969     if (!mDisposed && multiListener != null) {
970     handler.post(new Runnable() {
971     public void run() {
972     multiListener.onConsumeMultiFinished(purchases, results);
973     }
974     });
975     }
976     }
977     })).start();
978     }
979    
980     void logDebug(String msg) {
981     if (mDebugLog) Log.d(mDebugTag, msg);
982     }
983    
984     void logError(String msg) {
985     Log.e(mDebugTag, "In-app billing error: " + msg);
986     }
987    
988     void logWarn(String msg) {
989     Log.w(mDebugTag, "In-app billing warning: " + msg);
990     }
991     }

  ViewVC Help
Powered by ViewVC 1.1.20