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

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

Parent Directory Parent Directory | Revision Log Revision Log


Revision 2123 - (show annotations) (download)
Wed Mar 5 12:11:16 2014 UTC (10 years, 2 months ago) by torben
File size: 44347 byte(s)
Add billing code
1 /* 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