├── .gitignore ├── AndroidBillingLibrary ├── .classpath ├── .gitignore ├── .project ├── .settings │ └── org.eclipse.jdt.core.prefs ├── AndroidManifest.xml ├── proguard.cfg ├── project.properties ├── res │ └── values │ │ └── strings.xml └── src │ ├── com │ └── android │ │ └── vending │ │ └── billing │ │ └── IMarketBillingService.aidl │ └── net │ └── robotmedia │ └── billing │ ├── BillingController.java │ ├── BillingReceiver.java │ ├── BillingRequest.java │ ├── BillingService.java │ ├── IBillingObserver.java │ ├── helper │ ├── AbstractBillingActivity.java │ ├── AbstractBillingFragment.java │ └── AbstractBillingObserver.java │ ├── model │ ├── BillingDB.java │ ├── Transaction.java │ └── TransactionManager.java │ ├── security │ ├── DefaultSignatureValidator.java │ └── ISignatureValidator.java │ └── utils │ ├── AESObfuscator.java │ ├── Base64.java │ ├── Base64DecoderException.java │ ├── Compatibility.java │ ├── Installation.java │ └── Security.java ├── AndroidBillingLibraryTest ├── .classpath ├── .gitignore ├── .project ├── .settings │ └── org.eclipse.jdt.core.prefs ├── AndroidManifest.xml ├── proguard.cfg ├── project.properties ├── res │ └── values │ │ └── strings.xml └── src │ └── net │ └── robotmedia │ └── billing │ ├── BillingControllerTest.java │ ├── BillingReceiverTest.java │ ├── BillingServiceTest.java │ ├── helper │ ├── AbstractBillingActivityTest.java │ ├── MockBillingActivity.java │ └── MockBillingObserver.java │ ├── model │ ├── BillingDBTest.java │ ├── TransactionManagerTest.java │ ├── TransactionTest.java │ └── request │ │ └── ResponseCodeTest.java │ └── utils │ ├── CompatibilityTest.java │ └── InstallationTest.java ├── DungeonsRedux ├── .classpath ├── .gitignore ├── .project ├── .settings │ └── org.eclipse.jdt.core.prefs ├── AndroidManifest.xml ├── README ├── project.properties ├── res │ ├── drawable-hdpi │ │ └── icon.png │ ├── drawable-ldpi │ │ └── icon.png │ ├── drawable-mdpi │ │ └── icon.png │ ├── layout │ │ ├── item_row.xml │ │ └── main.xml │ └── values │ │ ├── colors.xml │ │ └── strings.xml └── src │ └── net │ └── robotmedia │ └── billing │ └── example │ ├── Application.java │ ├── Dungeons.java │ └── auxiliary │ ├── CatalogAdapter.java │ └── CatalogEntry.java └── README.mdown /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .metadata -------------------------------------------------------------------------------- /AndroidBillingLibrary/.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /AndroidBillingLibrary/.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | gen -------------------------------------------------------------------------------- /AndroidBillingLibrary/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | AndroidBillingLibrary 4 | 5 | 6 | 7 | 8 | 9 | com.android.ide.eclipse.adt.ResourceManagerBuilder 10 | 11 | 12 | 13 | 14 | com.android.ide.eclipse.adt.PreCompilerBuilder 15 | 16 | 17 | 18 | 19 | org.eclipse.jdt.core.javabuilder 20 | 21 | 22 | 23 | 24 | com.android.ide.eclipse.adt.ApkBuilder 25 | 26 | 27 | 28 | 29 | 30 | com.android.ide.eclipse.adt.AndroidNature 31 | org.eclipse.jdt.core.javanature 32 | 33 | 34 | -------------------------------------------------------------------------------- /AndroidBillingLibrary/.settings/org.eclipse.jdt.core.prefs: -------------------------------------------------------------------------------- 1 | eclipse.preferences.version=1 2 | org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.5 3 | org.eclipse.jdt.core.compiler.compliance=1.5 4 | org.eclipse.jdt.core.compiler.source=1.5 5 | -------------------------------------------------------------------------------- /AndroidBillingLibrary/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /AndroidBillingLibrary/proguard.cfg: -------------------------------------------------------------------------------- 1 | -optimizationpasses 5 2 | -dontusemixedcaseclassnames 3 | -dontskipnonpubliclibraryclasses 4 | -dontpreverify 5 | -verbose 6 | -optimizations !code/simplification/arithmetic,!field/*,!class/merging/* 7 | 8 | -keep public class * extends android.app.Activity 9 | -keep public class * extends android.app.Application 10 | -keep public class * extends android.app.Service 11 | -keep public class * extends android.content.BroadcastReceiver 12 | -keep public class * extends android.content.ContentProvider 13 | -keep public class * extends android.app.backup.BackupAgentHelper 14 | -keep public class * extends android.preference.Preference 15 | -keep public class com.android.vending.licensing.ILicensingService 16 | 17 | -keepclasseswithmembernames class * { 18 | native ; 19 | } 20 | 21 | -keepclasseswithmembernames class * { 22 | public (android.content.Context, android.util.AttributeSet); 23 | } 24 | 25 | -keepclasseswithmembernames class * { 26 | public (android.content.Context, android.util.AttributeSet, int); 27 | } 28 | 29 | -keepclassmembers enum * { 30 | public static **[] values(); 31 | public static ** valueOf(java.lang.String); 32 | } 33 | 34 | -keep class * implements android.os.Parcelable { 35 | public static final android.os.Parcelable$Creator *; 36 | } 37 | -------------------------------------------------------------------------------- /AndroidBillingLibrary/project.properties: -------------------------------------------------------------------------------- 1 | # This file is automatically generated by Android Tools. 2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED! 3 | # 4 | # This file must be checked in Version Control Systems. 5 | # 6 | # To customize properties used by the Ant build system edit 7 | # "ant.properties", and override values to adapt the script to your 8 | # project structure. 9 | # 10 | # To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): 11 | #proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt 12 | 13 | android.library=true 14 | # Project target. 15 | target=android-13 16 | -------------------------------------------------------------------------------- /AndroidBillingLibrary/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | AndroidBillingLibraryTest 4 | 5 | -------------------------------------------------------------------------------- /AndroidBillingLibrary/src/com/android/vending/billing/IMarketBillingService.aidl: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2010 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.android.vending.billing; 18 | 19 | import android.os.Bundle; 20 | 21 | interface IMarketBillingService { 22 | /** Given the arguments in bundle form, returns a bundle for results. */ 23 | Bundle sendBillingRequest(in Bundle bundle); 24 | } 25 | -------------------------------------------------------------------------------- /AndroidBillingLibrary/src/net/robotmedia/billing/BillingController.java: -------------------------------------------------------------------------------- 1 | /* Copyright 2011 Robot Media SL (http://www.robotmedia.net) 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 net.robotmedia.billing; 17 | 18 | import java.util.ArrayList; 19 | import java.util.HashMap; 20 | import java.util.HashSet; 21 | import java.util.List; 22 | import java.util.Set; 23 | 24 | import org.json.JSONArray; 25 | import org.json.JSONException; 26 | import org.json.JSONObject; 27 | 28 | import net.robotmedia.billing.model.Transaction; 29 | import net.robotmedia.billing.model.TransactionManager; 30 | import net.robotmedia.billing.security.DefaultSignatureValidator; 31 | import net.robotmedia.billing.security.ISignatureValidator; 32 | import net.robotmedia.billing.utils.Compatibility; 33 | import net.robotmedia.billing.utils.Security; 34 | 35 | import android.app.Activity; 36 | import android.app.PendingIntent; 37 | import android.app.PendingIntent.CanceledException; 38 | import android.content.Context; 39 | import android.content.Intent; 40 | import android.text.TextUtils; 41 | import android.util.Log; 42 | 43 | public class BillingController { 44 | 45 | public static enum BillingStatus { 46 | UNKNOWN, SUPPORTED, UNSUPPORTED 47 | } 48 | 49 | /** 50 | * Used to provide on-demand values to the billing controller. 51 | */ 52 | public interface IConfiguration { 53 | 54 | /** 55 | * Returns a salt for the obfuscation of purchases in local memory. 56 | * 57 | * @return array of 20 random bytes. 58 | */ 59 | public byte[] getObfuscationSalt(); 60 | 61 | /** 62 | * Returns the public key used to verify the signature of responses of 63 | * the Market Billing service. 64 | * 65 | * @return Base64 encoded public key. 66 | */ 67 | public String getPublicKey(); 68 | } 69 | 70 | private static BillingStatus billingStatus = BillingStatus.UNKNOWN; 71 | private static BillingStatus subscriptionStatus = BillingStatus.UNKNOWN; 72 | 73 | private static Set automaticConfirmations = new HashSet(); 74 | private static IConfiguration configuration = null; 75 | private static boolean debug = false; 76 | private static ISignatureValidator validator = null; 77 | 78 | private static final String JSON_NONCE = "nonce"; 79 | private static final String JSON_ORDERS = "orders"; 80 | private static HashMap> manualConfirmations = new HashMap>(); 81 | 82 | private static Set observers = new HashSet(); 83 | 84 | public static final String LOG_TAG = "Billing"; 85 | 86 | private static HashMap pendingRequests = new HashMap(); 87 | 88 | /** 89 | * Adds the specified notification to the set of manual confirmations of the 90 | * specified item. 91 | * 92 | * @param itemId 93 | * id of the item. 94 | * @param notificationId 95 | * id of the notification. 96 | */ 97 | private static final void addManualConfirmation(String itemId, String notificationId) { 98 | Set notifications = manualConfirmations.get(itemId); 99 | if (notifications == null) { 100 | notifications = new HashSet(); 101 | manualConfirmations.put(itemId, notifications); 102 | } 103 | notifications.add(notificationId); 104 | } 105 | 106 | /** 107 | * Returns the in-app product billing support status, and checks it 108 | * asynchronously if it is currently unknown. Observers will receive a 109 | * {@link IBillingObserver#onBillingChecked(boolean)} notification in either 110 | * case. 111 | *

112 | * In-app product support does not imply subscription support. To check if 113 | * subscriptions are supported, use 114 | * {@link BillingController#checkSubscriptionSupported(Context)}. 115 | *

116 | * 117 | * @param context 118 | * @return the current in-app product billing support status (unknown, 119 | * supported or unsupported). If it is unsupported, subscriptions 120 | * are also unsupported. 121 | * @see IBillingObserver#onBillingChecked(boolean) 122 | * @see BillingController#checkSubscriptionSupported(Context) 123 | */ 124 | public static BillingStatus checkBillingSupported(Context context) { 125 | if (billingStatus == BillingStatus.UNKNOWN) { 126 | BillingService.checkBillingSupported(context); 127 | } else { 128 | boolean supported = billingStatus == BillingStatus.SUPPORTED; 129 | onBillingChecked(supported); 130 | } 131 | return billingStatus; 132 | } 133 | 134 | /** 135 | *

136 | * Returns the subscription billing support status, and checks it 137 | * asynchronously if it is currently unknown. Observers will receive a 138 | * {@link IBillingObserver#onSubscriptionChecked(boolean)} notification in 139 | * either case. 140 | *

141 | *

142 | * No support for subscriptions does not imply that in-app products are also 143 | * unsupported. To check if in-app products are supported, use 144 | * {@link BillingController#checkBillingSupported(Context)}. 145 | *

146 | * 147 | * @param context 148 | * @return the current subscription billing status (unknown, supported or 149 | * unsupported). If it is supported, in-app products are also 150 | * supported. 151 | * @see IBillingObserver#onSubscriptionChecked(boolean) 152 | * @see BillingController#checkBillingSupported(Context) 153 | */ 154 | public static BillingStatus checkSubscriptionSupported(Context context) { 155 | if (subscriptionStatus == BillingStatus.UNKNOWN) { 156 | BillingService.checkSubscriptionSupported(context); 157 | } else { 158 | boolean supported = subscriptionStatus == BillingStatus.SUPPORTED; 159 | onSubscriptionChecked(supported); 160 | } 161 | return subscriptionStatus; 162 | } 163 | 164 | /** 165 | * Requests to confirm all pending notifications for the specified item. 166 | * 167 | * @param context 168 | * @param itemId 169 | * id of the item whose purchase must be confirmed. 170 | * @return true if pending notifications for this item were found, false 171 | * otherwise. 172 | */ 173 | public static boolean confirmNotifications(Context context, String itemId) { 174 | final Set notifications = manualConfirmations.get(itemId); 175 | if (notifications != null) { 176 | confirmNotifications(context, notifications.toArray(new String[] {})); 177 | return true; 178 | } else { 179 | return false; 180 | } 181 | } 182 | 183 | /** 184 | * Requests to confirm all specified notifications. 185 | * 186 | * @param context 187 | * @param notifyIds 188 | * array with the ids of all the notifications to confirm. 189 | */ 190 | private static void confirmNotifications(Context context, String[] notifyIds) { 191 | BillingService.confirmNotifications(context, notifyIds); 192 | } 193 | 194 | /** 195 | * Returns the number of purchases for the specified item. Refunded and 196 | * cancelled purchases are not subtracted. See 197 | * {@link #countPurchasesNet(Context, String)} if they need to be. 198 | * 199 | * @param context 200 | * @param itemId 201 | * id of the item whose purchases will be counted. 202 | * @return number of purchases for the specified item. 203 | */ 204 | public static int countPurchases(Context context, String itemId) { 205 | final byte[] salt = getSalt(); 206 | itemId = salt != null ? Security.obfuscate(context, salt, itemId) : itemId; 207 | return TransactionManager.countPurchases(context, itemId); 208 | } 209 | 210 | protected static void debug(String message) { 211 | if (debug) { 212 | Log.d(LOG_TAG, message); 213 | } 214 | } 215 | 216 | /** 217 | * Requests purchase information for the specified notification. Immediately 218 | * followed by a call to 219 | * {@link #onPurchaseInformationResponse(long, boolean)} and later to 220 | * {@link #onPurchaseStateChanged(Context, String, String)}, if the request 221 | * is successful. 222 | * 223 | * @param context 224 | * @param notifyId 225 | * id of the notification whose purchase information is 226 | * requested. 227 | */ 228 | private static void getPurchaseInformation(Context context, String notifyId) { 229 | final long nonce = Security.generateNonce(); 230 | BillingService.getPurchaseInformation(context, new String[] { notifyId }, nonce); 231 | } 232 | 233 | /** 234 | * Gets the salt from the configuration and logs a warning if it's null. 235 | * 236 | * @return salt. 237 | */ 238 | private static byte[] getSalt() { 239 | byte[] salt = null; 240 | if (configuration == null || ((salt = configuration.getObfuscationSalt()) == null)) { 241 | Log.w(LOG_TAG, "Can't (un)obfuscate purchases without salt"); 242 | } 243 | return salt; 244 | } 245 | 246 | /** 247 | * Lists all transactions stored locally, including cancellations and 248 | * refunds. 249 | * 250 | * @param context 251 | * @return list of transactions. 252 | */ 253 | public static List getTransactions(Context context) { 254 | List transactions = TransactionManager.getTransactions(context); 255 | unobfuscate(context, transactions); 256 | return transactions; 257 | } 258 | 259 | /** 260 | * Lists all transactions of the specified item, stored locally. 261 | * 262 | * @param context 263 | * @param itemId 264 | * id of the item whose transactions will be returned. 265 | * @return list of transactions. 266 | */ 267 | public static List getTransactions(Context context, String itemId) { 268 | final byte[] salt = getSalt(); 269 | itemId = salt != null ? Security.obfuscate(context, salt, itemId) : itemId; 270 | List transactions = TransactionManager.getTransactions(context, itemId); 271 | unobfuscate(context, transactions); 272 | return transactions; 273 | } 274 | 275 | /** 276 | * Returns true if the specified item has been registered as purchased in 277 | * local memory, false otherwise. Also note that the item might have been 278 | * purchased in another installation, but not yet registered in this one. 279 | * 280 | * @param context 281 | * @param itemId 282 | * item id. 283 | * @return true if the specified item is purchased, false otherwise. 284 | */ 285 | public static boolean isPurchased(Context context, String itemId) { 286 | final byte[] salt = getSalt(); 287 | itemId = salt != null ? Security.obfuscate(context, salt, itemId) : itemId; 288 | return TransactionManager.isPurchased(context, itemId); 289 | } 290 | 291 | /** 292 | * Notifies observers of the purchase state change of the specified item. 293 | * 294 | * @param itemId 295 | * id of the item whose purchase state has changed. 296 | * @param state 297 | * new purchase state of the item. 298 | */ 299 | private static void notifyPurchaseStateChange(String itemId, Transaction.PurchaseState state) { 300 | for (IBillingObserver o : observers) { 301 | o.onPurchaseStateChanged(itemId, state); 302 | } 303 | } 304 | 305 | /** 306 | * Obfuscates the specified purchase. Only the order id, product id and 307 | * developer payload are obfuscated. 308 | * 309 | * @param context 310 | * @param purchase 311 | * purchase to be obfuscated. 312 | * @see #unobfuscate(Context, Transaction) 313 | */ 314 | static void obfuscate(Context context, Transaction purchase) { 315 | final byte[] salt = getSalt(); 316 | if (salt == null) { 317 | return; 318 | } 319 | purchase.orderId = Security.obfuscate(context, salt, purchase.orderId); 320 | purchase.productId = Security.obfuscate(context, salt, purchase.productId); 321 | purchase.developerPayload = Security.obfuscate(context, salt, purchase.developerPayload); 322 | } 323 | 324 | /** 325 | * Called after the response to a 326 | * {@link net.robotmedia.billing.request.CheckBillingSupported} request is 327 | * received. 328 | * 329 | * @param supported 330 | */ 331 | protected static void onBillingChecked(boolean supported) { 332 | billingStatus = supported ? BillingStatus.SUPPORTED : BillingStatus.UNSUPPORTED; 333 | if (billingStatus == BillingStatus.UNSUPPORTED) { // Save us the 334 | // subscription 335 | // check 336 | subscriptionStatus = BillingStatus.UNSUPPORTED; 337 | } 338 | for (IBillingObserver o : observers) { 339 | o.onBillingChecked(supported); 340 | } 341 | } 342 | 343 | /** 344 | * Called when an IN_APP_NOTIFY message is received. 345 | * 346 | * @param context 347 | * @param notifyId 348 | * notification id. 349 | */ 350 | protected static void onNotify(Context context, String notifyId) { 351 | debug("Notification " + notifyId + " available"); 352 | 353 | getPurchaseInformation(context, notifyId); 354 | } 355 | 356 | /** 357 | * Called after the response to a 358 | * {@link net.robotmedia.billing.request.RequestPurchase} request is 359 | * received. 360 | * 361 | * @param itemId 362 | * id of the item whose purchase was requested. 363 | * @param purchaseIntent 364 | * intent to purchase the item. 365 | */ 366 | protected static void onPurchaseIntent(String itemId, PendingIntent purchaseIntent) { 367 | for (IBillingObserver o : observers) { 368 | o.onPurchaseIntent(itemId, purchaseIntent); 369 | } 370 | } 371 | 372 | /** 373 | * Called after the response to a 374 | * {@link net.robotmedia.billing.request.GetPurchaseInformation} request is 375 | * received. Registers all transactions in local memory and confirms those 376 | * who can be confirmed automatically. 377 | * 378 | * @param context 379 | * @param signedData 380 | * signed JSON data received from the Market Billing service. 381 | * @param signature 382 | * data signature. 383 | */ 384 | protected static void onPurchaseStateChanged(Context context, String signedData, String signature) { 385 | debug("Purchase state changed"); 386 | 387 | if (TextUtils.isEmpty(signedData)) { 388 | Log.w(LOG_TAG, "Signed data is empty"); 389 | return; 390 | } else { 391 | debug(signedData); 392 | } 393 | 394 | if (!debug) { 395 | if (TextUtils.isEmpty(signature)) { 396 | Log.w(LOG_TAG, "Empty signature requires debug mode"); 397 | return; 398 | } 399 | final ISignatureValidator validator = BillingController.validator != null ? BillingController.validator 400 | : new DefaultSignatureValidator(BillingController.configuration); 401 | if (!validator.validate(signedData, signature)) { 402 | Log.w(LOG_TAG, "Signature does not match data."); 403 | return; 404 | } 405 | } 406 | 407 | List purchases; 408 | try { 409 | JSONObject jObject = new JSONObject(signedData); 410 | if (!verifyNonce(jObject)) { 411 | Log.w(LOG_TAG, "Invalid nonce"); 412 | return; 413 | } 414 | purchases = parsePurchases(jObject); 415 | } catch (JSONException e) { 416 | Log.e(LOG_TAG, "JSON exception: ", e); 417 | return; 418 | } 419 | 420 | ArrayList confirmations = new ArrayList(); 421 | for (Transaction p : purchases) { 422 | if (p.notificationId != null && automaticConfirmations.contains(p.productId)) { 423 | confirmations.add(p.notificationId); 424 | } else { 425 | // TODO: Discriminate between purchases, cancellations and 426 | // refunds. 427 | addManualConfirmation(p.productId, p.notificationId); 428 | } 429 | storeTransaction(context, p); 430 | notifyPurchaseStateChange(p.productId, p.purchaseState); 431 | } 432 | if (!confirmations.isEmpty()) { 433 | final String[] notifyIds = confirmations.toArray(new String[confirmations.size()]); 434 | confirmNotifications(context, notifyIds); 435 | } 436 | } 437 | 438 | /** 439 | * Called after a {@link net.robotmedia.billing.BillingRequest} is sent. 440 | * 441 | * @param requestId 442 | * the id the request. 443 | * @param request 444 | * the billing request. 445 | */ 446 | protected static void onRequestSent(long requestId, BillingRequest request) { 447 | debug("Request " + requestId + " of type " + request.getRequestType() + " sent"); 448 | 449 | if (request.isSuccess()) { 450 | pendingRequests.put(requestId, request); 451 | } else if (request.hasNonce()) { 452 | Security.removeNonce(request.getNonce()); 453 | } 454 | } 455 | 456 | /** 457 | * Called after a {@link net.robotmedia.billing.BillingRequest} is sent. 458 | * 459 | * @param context 460 | * @param requestId 461 | * the id of the request. 462 | * @param responseCode 463 | * the response code. 464 | * @see net.robotmedia.billing.request.ResponseCode 465 | */ 466 | protected static void onResponseCode(Context context, long requestId, int responseCode) { 467 | final BillingRequest.ResponseCode response = BillingRequest.ResponseCode.valueOf(responseCode); 468 | debug("Request " + requestId + " received response " + response); 469 | 470 | final BillingRequest request = pendingRequests.get(requestId); 471 | if (request != null) { 472 | pendingRequests.remove(requestId); 473 | request.onResponseCode(response); 474 | } 475 | } 476 | 477 | /** 478 | * Called after the response to a 479 | * {@link net.robotmedia.billing.request.CheckSubscriptionSupported} request 480 | * is received. 481 | * 482 | * @param supported 483 | */ 484 | protected static void onSubscriptionChecked(boolean supported) { 485 | subscriptionStatus = supported ? BillingStatus.SUPPORTED : BillingStatus.UNSUPPORTED; 486 | if (subscriptionStatus == BillingStatus.SUPPORTED) { // Save us the 487 | // billing check 488 | billingStatus = BillingStatus.SUPPORTED; 489 | } 490 | for (IBillingObserver o : observers) { 491 | o.onSubscriptionChecked(supported); 492 | } 493 | } 494 | 495 | protected static void onTransactionsRestored() { 496 | for (IBillingObserver o : observers) { 497 | o.onTransactionsRestored(); 498 | } 499 | } 500 | 501 | /** 502 | * Parse all purchases from the JSON data received from the Market Billing 503 | * service. 504 | * 505 | * @param data 506 | * JSON data received from the Market Billing service. 507 | * @return list of purchases. 508 | * @throws JSONException 509 | * if the data couldn't be properly parsed. 510 | */ 511 | private static List parsePurchases(JSONObject data) throws JSONException { 512 | ArrayList purchases = new ArrayList(); 513 | JSONArray orders = data.optJSONArray(JSON_ORDERS); 514 | int numTransactions = 0; 515 | if (orders != null) { 516 | numTransactions = orders.length(); 517 | } 518 | for (int i = 0; i < numTransactions; i++) { 519 | JSONObject jElement = orders.getJSONObject(i); 520 | Transaction p = Transaction.parse(jElement); 521 | purchases.add(p); 522 | } 523 | return purchases; 524 | } 525 | 526 | /** 527 | * Registers the specified billing observer. 528 | * 529 | * @param observer 530 | * the billing observer to add. 531 | * @return true if the observer wasn't previously registered, false 532 | * otherwise. 533 | * @see #unregisterObserver(IBillingObserver) 534 | */ 535 | public static boolean registerObserver(IBillingObserver observer) { 536 | return observers.add(observer); 537 | } 538 | 539 | /** 540 | * Requests the purchase of the specified item. The transaction will not be 541 | * confirmed automatically. 542 | *

543 | * For subscriptions, use {@link #requestSubscription(Context, String)} 544 | * instead. 545 | *

546 | * 547 | * @param context 548 | * @param itemId 549 | * id of the item to be purchased. 550 | * @see #requestPurchase(Context, String, boolean, String) 551 | */ 552 | public static void requestPurchase(Context context, String itemId) { 553 | requestPurchase(context, itemId, false, null); 554 | } 555 | 556 | /** 557 | *

558 | * Requests the purchase of the specified item with optional automatic 559 | * confirmation. 560 | *

561 | *

562 | * For subscriptions, use 563 | * {@link #requestSubscription(Context, String, boolean, String)} instead. 564 | *

565 | * 566 | * @param context 567 | * @param itemId 568 | * id of the item to be purchased. 569 | * @param confirm 570 | * if true, the transaction will be confirmed automatically. If 571 | * false, the transaction will have to be confirmed with a call 572 | * to {@link #confirmNotifications(Context, String)}. 573 | * @param developerPayload 574 | * a developer-specified string that contains supplemental 575 | * information about the order. 576 | * @see IBillingObserver#onPurchaseIntent(String, PendingIntent) 577 | */ 578 | public static void requestPurchase(Context context, String itemId, boolean confirm, String developerPayload) { 579 | if (confirm) { 580 | automaticConfirmations.add(itemId); 581 | } 582 | BillingService.requestPurchase(context, itemId, developerPayload); 583 | } 584 | 585 | /** 586 | * Requests the purchase of the specified subscription item. The transaction 587 | * will not be confirmed automatically. 588 | * 589 | * @param context 590 | * @param itemId 591 | * id of the item to be purchased. 592 | * @see #requestSubscription(Context, String, boolean, String) 593 | */ 594 | public static void requestSubscription(Context context, String itemId) { 595 | requestSubscription(context, itemId, false, null); 596 | } 597 | 598 | /** 599 | * Requests the purchase of the specified subscription item with optional 600 | * automatic confirmation. 601 | * 602 | * @param context 603 | * @param itemId 604 | * id of the item to be purchased. 605 | * @param confirm 606 | * if true, the transaction will be confirmed automatically. If 607 | * false, the transaction will have to be confirmed with a call 608 | * to {@link #confirmNotifications(Context, String)}. 609 | * @param developerPayload 610 | * a developer-specified string that contains supplemental 611 | * information about the order. 612 | * @see IBillingObserver#onPurchaseIntent(String, PendingIntent) 613 | */ 614 | public static void requestSubscription(Context context, String itemId, boolean confirm, String developerPayload) { 615 | if (confirm) { 616 | automaticConfirmations.add(itemId); 617 | } 618 | BillingService.requestSubscription(context, itemId, developerPayload); 619 | } 620 | 621 | /** 622 | * Requests to restore all transactions. 623 | * 624 | * @param context 625 | */ 626 | public static void restoreTransactions(Context context) { 627 | final long nonce = Security.generateNonce(); 628 | BillingService.restoreTransations(context, nonce); 629 | } 630 | 631 | /** 632 | * Sets the configuration instance of the controller. 633 | * 634 | * @param config 635 | * configuration instance. 636 | */ 637 | public static void setConfiguration(IConfiguration config) { 638 | configuration = config; 639 | } 640 | 641 | /** 642 | * Sets debug mode. 643 | * 644 | * @param value 645 | */ 646 | public static final void setDebug(boolean value) { 647 | debug = value; 648 | } 649 | 650 | /** 651 | * Sets a custom signature validator. If no custom signature validator is 652 | * provided, 653 | * {@link net.robotmedia.billing.signature.DefaultSignatureValidator} will 654 | * be used. 655 | * 656 | * @param validator 657 | * signature validator instance. 658 | */ 659 | public static void setSignatureValidator(ISignatureValidator validator) { 660 | BillingController.validator = validator; 661 | } 662 | 663 | /** 664 | * Starts the specified purchase intent with the specified activity. 665 | * 666 | * @param activity 667 | * @param purchaseIntent 668 | * purchase intent. 669 | * @param intent 670 | */ 671 | public static void startPurchaseIntent(Activity activity, PendingIntent purchaseIntent, Intent intent) { 672 | if (Compatibility.isStartIntentSenderSupported()) { 673 | // This is on Android 2.0 and beyond. The in-app buy page activity 674 | // must be on the activity stack of the application. 675 | Compatibility.startIntentSender(activity, purchaseIntent.getIntentSender(), intent); 676 | } else { 677 | // This is on Android version 1.6. The in-app buy page activity must 678 | // be on its own separate activity stack instead of on the activity 679 | // stack of the application. 680 | try { 681 | purchaseIntent.send(activity, 0 /* code */, intent); 682 | } catch (CanceledException e) { 683 | Log.e(LOG_TAG, "Error starting purchase intent", e); 684 | } 685 | } 686 | } 687 | 688 | static void storeTransaction(Context context, Transaction t) { 689 | final Transaction t2 = t.clone(); 690 | obfuscate(context, t2); 691 | TransactionManager.addTransaction(context, t2); 692 | } 693 | 694 | static void unobfuscate(Context context, List transactions) { 695 | for (Transaction p : transactions) { 696 | unobfuscate(context, p); 697 | } 698 | } 699 | 700 | /** 701 | * Unobfuscate the specified purchase. 702 | * 703 | * @param context 704 | * @param purchase 705 | * purchase to unobfuscate. 706 | * @see #obfuscate(Context, Transaction) 707 | */ 708 | static void unobfuscate(Context context, Transaction purchase) { 709 | final byte[] salt = getSalt(); 710 | if (salt == null) { 711 | return; 712 | } 713 | purchase.orderId = Security.unobfuscate(context, salt, purchase.orderId); 714 | purchase.productId = Security.unobfuscate(context, salt, purchase.productId); 715 | purchase.developerPayload = Security.unobfuscate(context, salt, purchase.developerPayload); 716 | } 717 | 718 | /** 719 | * Unregisters the specified billing observer. 720 | * 721 | * @param observer 722 | * the billing observer to unregister. 723 | * @return true if the billing observer was unregistered, false otherwise. 724 | * @see #registerObserver(IBillingObserver) 725 | */ 726 | public static boolean unregisterObserver(IBillingObserver observer) { 727 | return observers.remove(observer); 728 | } 729 | 730 | private static boolean verifyNonce(JSONObject data) { 731 | long nonce = data.optLong(JSON_NONCE); 732 | if (Security.isNonceKnown(nonce)) { 733 | Security.removeNonce(nonce); 734 | return true; 735 | } else { 736 | return false; 737 | } 738 | } 739 | 740 | protected static void onRequestPurchaseResponse(String itemId, BillingRequest.ResponseCode response) { 741 | for (IBillingObserver o : observers) { 742 | o.onRequestPurchaseResponse(itemId, response); 743 | } 744 | } 745 | 746 | } 747 | -------------------------------------------------------------------------------- /AndroidBillingLibrary/src/net/robotmedia/billing/BillingReceiver.java: -------------------------------------------------------------------------------- 1 | /* Copyright 2011 Robot Media SL (http://www.robotmedia.net) 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 net.robotmedia.billing; 17 | 18 | import android.content.BroadcastReceiver; 19 | import android.content.Context; 20 | import android.content.Intent; 21 | import android.util.Log; 22 | 23 | public class BillingReceiver extends BroadcastReceiver { 24 | 25 | static final String ACTION_NOTIFY = "com.android.vending.billing.IN_APP_NOTIFY"; 26 | static final String ACTION_RESPONSE_CODE = 27 | "com.android.vending.billing.RESPONSE_CODE"; 28 | static final String ACTION_PURCHASE_STATE_CHANGED = 29 | "com.android.vending.billing.PURCHASE_STATE_CHANGED"; 30 | 31 | static final String EXTRA_NOTIFICATION_ID = "notification_id"; 32 | static final String EXTRA_INAPP_SIGNED_DATA = "inapp_signed_data"; 33 | static final String EXTRA_INAPP_SIGNATURE = "inapp_signature"; 34 | static final String EXTRA_REQUEST_ID = "request_id"; 35 | static final String EXTRA_RESPONSE_CODE = "response_code"; 36 | 37 | @Override 38 | public void onReceive(Context context, Intent intent) { 39 | final String action = intent.getAction(); 40 | BillingController.debug("Received " + action); 41 | 42 | if (ACTION_PURCHASE_STATE_CHANGED.equals(action)) { 43 | purchaseStateChanged(context, intent); 44 | } else if (ACTION_NOTIFY.equals(action)) { 45 | notify(context, intent); 46 | } else if (ACTION_RESPONSE_CODE.equals(action)) { 47 | responseCode(context, intent); 48 | } else { 49 | Log.w(this.getClass().getSimpleName(), "Unexpected action: " + action); 50 | } 51 | } 52 | 53 | private void purchaseStateChanged(Context context, Intent intent) { 54 | final String signedData = intent.getStringExtra(EXTRA_INAPP_SIGNED_DATA); 55 | final String signature = intent.getStringExtra(EXTRA_INAPP_SIGNATURE); 56 | BillingController.onPurchaseStateChanged(context, signedData, signature); 57 | } 58 | 59 | private void notify(Context context, Intent intent) { 60 | String notifyId = intent.getStringExtra(EXTRA_NOTIFICATION_ID); 61 | BillingController.onNotify(context, notifyId); 62 | } 63 | 64 | private void responseCode(Context context, Intent intent) { 65 | final long requestId = intent.getLongExtra(EXTRA_REQUEST_ID, -1); 66 | final int responseCode = intent.getIntExtra(EXTRA_RESPONSE_CODE, 0); 67 | BillingController.onResponseCode(context, requestId, responseCode); 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /AndroidBillingLibrary/src/net/robotmedia/billing/BillingRequest.java: -------------------------------------------------------------------------------- 1 | /* Copyright 2011 Robot Media SL (http://www.robotmedia.net) 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 net.robotmedia.billing; 17 | 18 | import android.app.PendingIntent; 19 | import android.os.Bundle; 20 | import android.os.RemoteException; 21 | import android.util.Log; 22 | 23 | import com.android.vending.billing.IMarketBillingService; 24 | 25 | public abstract class BillingRequest { 26 | 27 | public static class CheckBillingSupported extends BillingRequest { 28 | 29 | public CheckBillingSupported(String packageName, int startId) { 30 | super(packageName, startId); 31 | } 32 | 33 | @Override 34 | public String getRequestType() { 35 | return REQUEST_TYPE_CHECK_BILLING_SUPPORTED; 36 | } 37 | 38 | @Override 39 | protected void processOkResponse(Bundle response) { 40 | final boolean supported = this.isSuccess(); 41 | BillingController.onBillingChecked(supported); 42 | } 43 | 44 | } 45 | 46 | public static class CheckSubscriptionSupported extends BillingRequest { 47 | 48 | public CheckSubscriptionSupported(String packageName, int startId) { 49 | super(packageName, startId); 50 | } 51 | 52 | @Override 53 | protected int getAPIVersion() { 54 | return 2; 55 | }; 56 | 57 | @Override 58 | public String getRequestType() { 59 | return REQUEST_TYPE_CHECK_BILLING_SUPPORTED; 60 | } 61 | 62 | @Override 63 | protected void processOkResponse(Bundle response) { 64 | final boolean supported = this.isSuccess(); 65 | BillingController.onSubscriptionChecked(supported); 66 | } 67 | 68 | @Override 69 | protected void addParams(Bundle request) { 70 | request.putString(KEY_ITEM_TYPE, ITEM_TYPE_SUBSCRIPTION); 71 | } 72 | 73 | } 74 | 75 | public static class ConfirmNotifications extends BillingRequest { 76 | 77 | private String[] notifyIds; 78 | 79 | private static final String KEY_NOTIFY_IDS = "NOTIFY_IDS"; 80 | 81 | public ConfirmNotifications(String packageName, int startId, String[] notifyIds) { 82 | super(packageName, startId); 83 | this.notifyIds = notifyIds; 84 | } 85 | 86 | @Override 87 | protected void addParams(Bundle request) { 88 | request.putStringArray(KEY_NOTIFY_IDS, notifyIds); 89 | } 90 | 91 | @Override 92 | public String getRequestType() { 93 | return "CONFIRM_NOTIFICATIONS"; 94 | } 95 | 96 | } 97 | public static class GetPurchaseInformation extends BillingRequest { 98 | 99 | private String[] notifyIds; 100 | 101 | private static final String KEY_NOTIFY_IDS = "NOTIFY_IDS"; 102 | 103 | public GetPurchaseInformation(String packageName, int startId, String[] notifyIds) { 104 | super(packageName,startId); 105 | this.notifyIds = notifyIds; 106 | } 107 | 108 | @Override 109 | protected void addParams(Bundle request) { 110 | request.putStringArray(KEY_NOTIFY_IDS, notifyIds); 111 | } 112 | 113 | @Override 114 | public String getRequestType() { 115 | return "GET_PURCHASE_INFORMATION"; 116 | } 117 | 118 | @Override public boolean hasNonce() { return true; } 119 | 120 | } 121 | 122 | public static class RequestPurchase extends BillingRequest { 123 | 124 | private String itemId; 125 | private String developerPayload; 126 | 127 | private static final String KEY_ITEM_ID = "ITEM_ID"; 128 | private static final String KEY_DEVELOPER_PAYLOAD = "DEVELOPER_PAYLOAD"; 129 | private static final String KEY_PURCHASE_INTENT = "PURCHASE_INTENT"; 130 | 131 | public RequestPurchase(String packageName, int startId, String itemId, String developerPayload) { 132 | super(packageName, startId); 133 | this.itemId = itemId; 134 | this.developerPayload = developerPayload; 135 | } 136 | 137 | @Override 138 | protected void addParams(Bundle request) { 139 | request.putString(KEY_ITEM_ID, itemId); 140 | if (developerPayload != null) { 141 | request.putString(KEY_DEVELOPER_PAYLOAD, developerPayload); 142 | } 143 | } 144 | 145 | @Override 146 | public String getRequestType() { 147 | return "REQUEST_PURCHASE"; 148 | } 149 | 150 | @Override 151 | public void onResponseCode(ResponseCode response) { 152 | super.onResponseCode(response); 153 | BillingController.onRequestPurchaseResponse(itemId, response); 154 | } 155 | 156 | @Override 157 | protected void processOkResponse(Bundle response) { 158 | final PendingIntent purchaseIntent = response.getParcelable(KEY_PURCHASE_INTENT); 159 | BillingController.onPurchaseIntent(itemId, purchaseIntent); 160 | } 161 | 162 | } 163 | 164 | public static class RequestSubscription extends RequestPurchase { 165 | 166 | public RequestSubscription(String packageName, int startId, String itemId, String developerPayload) { 167 | super(packageName, startId, itemId, developerPayload); 168 | } 169 | 170 | @Override 171 | protected void addParams(Bundle request) { 172 | super.addParams(request); 173 | request.putString(KEY_ITEM_TYPE, ITEM_TYPE_SUBSCRIPTION); 174 | } 175 | 176 | @Override 177 | protected int getAPIVersion() { 178 | return 2; 179 | } 180 | } 181 | 182 | public static enum ResponseCode { 183 | RESULT_OK, // 0 184 | RESULT_USER_CANCELED, // 1 185 | RESULT_SERVICE_UNAVAILABLE, // 2 186 | RESULT_BILLING_UNAVAILABLE, // 3 187 | RESULT_ITEM_UNAVAILABLE, // 4 188 | RESULT_DEVELOPER_ERROR, // 5 189 | RESULT_ERROR; // 6 190 | 191 | public static boolean isResponseOk(int response) { 192 | return ResponseCode.RESULT_OK.ordinal() == response; 193 | } 194 | 195 | // Converts from an ordinal value to the ResponseCode 196 | public static ResponseCode valueOf(int index) { 197 | ResponseCode[] values = ResponseCode.values(); 198 | if (index < 0 || index >= values.length) { 199 | return RESULT_ERROR; 200 | } 201 | return values[index]; 202 | } 203 | } 204 | public static class RestoreTransactions extends BillingRequest { 205 | 206 | public RestoreTransactions(String packageName, int startId) { 207 | super(packageName, startId); 208 | } 209 | 210 | @Override 211 | public String getRequestType() { 212 | return "RESTORE_TRANSACTIONS"; 213 | } 214 | 215 | @Override public boolean hasNonce() { return true; } 216 | 217 | @Override 218 | public void onResponseCode(ResponseCode response) { 219 | super.onResponseCode(response); 220 | if (response == ResponseCode.RESULT_OK) { 221 | BillingController.onTransactionsRestored(); 222 | } 223 | } 224 | 225 | } 226 | 227 | public static final String ITEM_TYPE_SUBSCRIPTION = "subs"; 228 | private static final String KEY_API_VERSION = "API_VERSION"; 229 | private static final String KEY_BILLING_REQUEST = "BILLING_REQUEST"; 230 | private static final String KEY_ITEM_TYPE = "ITEM_TYPE"; 231 | private static final String KEY_NONCE = "NONCE"; 232 | private static final String KEY_PACKAGE_NAME = "PACKAGE_NAME"; 233 | protected static final String KEY_REQUEST_ID = "REQUEST_ID"; 234 | private static final String KEY_RESPONSE_CODE = "RESPONSE_CODE"; 235 | private static final String REQUEST_TYPE_CHECK_BILLING_SUPPORTED = "CHECK_BILLING_SUPPORTED"; 236 | 237 | public static final long IGNORE_REQUEST_ID = -1; 238 | private String packageName; 239 | 240 | private int startId; 241 | private boolean success; 242 | private long nonce; 243 | public BillingRequest(String packageName,int startId) { 244 | this.packageName = packageName; 245 | this.startId=startId; 246 | } 247 | 248 | protected void addParams(Bundle request) { 249 | // Do nothing by default 250 | } 251 | 252 | protected int getAPIVersion() { 253 | return 1; 254 | } 255 | 256 | public long getNonce() { 257 | return nonce; 258 | } 259 | 260 | public abstract String getRequestType(); 261 | 262 | public boolean hasNonce() { 263 | return false; 264 | } 265 | 266 | public boolean isSuccess() { 267 | return success; 268 | } 269 | 270 | protected Bundle makeRequestBundle() { 271 | final Bundle request = new Bundle(); 272 | request.putString(KEY_BILLING_REQUEST, getRequestType()); 273 | request.putInt(KEY_API_VERSION, getAPIVersion()); 274 | request.putString(KEY_PACKAGE_NAME, packageName); 275 | if (hasNonce()) { 276 | request.putLong(KEY_NONCE, nonce); 277 | } 278 | return request; 279 | } 280 | 281 | public void onResponseCode(ResponseCode responde) { 282 | // Do nothing by default 283 | } 284 | 285 | protected void processOkResponse(Bundle response) { 286 | // Do nothing by default 287 | } 288 | 289 | public long run(IMarketBillingService mService) throws RemoteException { 290 | final Bundle request = makeRequestBundle(); 291 | addParams(request); 292 | final Bundle response; 293 | try { 294 | response = mService.sendBillingRequest(request); 295 | } catch (NullPointerException e) { 296 | Log.e(this.getClass().getSimpleName(), "Known IAB bug. See: http://code.google.com/p/marketbilling/issues/detail?id=25", e); 297 | return IGNORE_REQUEST_ID; 298 | } 299 | 300 | if (validateResponse(response)) { 301 | processOkResponse(response); 302 | return response.getLong(KEY_REQUEST_ID, IGNORE_REQUEST_ID); 303 | } else { 304 | return IGNORE_REQUEST_ID; 305 | } 306 | } 307 | 308 | public void setNonce(long nonce) { 309 | this.nonce = nonce; 310 | } 311 | 312 | protected boolean validateResponse(Bundle response) { 313 | final int responseCode = response.getInt(KEY_RESPONSE_CODE); 314 | success = ResponseCode.isResponseOk(responseCode); 315 | if (!success) { 316 | Log.w(this.getClass().getSimpleName(), "Error with response code " + ResponseCode.valueOf(responseCode)); 317 | } 318 | return success; 319 | } 320 | 321 | public int getStartId() { 322 | return startId; 323 | } 324 | 325 | } -------------------------------------------------------------------------------- /AndroidBillingLibrary/src/net/robotmedia/billing/BillingService.java: -------------------------------------------------------------------------------- 1 | /* Copyright 2011 Robot Media SL (http://www.robotmedia.net) 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 net.robotmedia.billing; 17 | 18 | import java.util.LinkedList; 19 | 20 | import static net.robotmedia.billing.BillingRequest.*; 21 | 22 | import net.robotmedia.billing.utils.Compatibility; 23 | 24 | import com.android.vending.billing.IMarketBillingService; 25 | 26 | import android.app.Service; 27 | import android.content.ComponentName; 28 | import android.content.Context; 29 | import android.content.Intent; 30 | import android.content.ServiceConnection; 31 | import android.os.IBinder; 32 | import android.os.RemoteException; 33 | import android.util.Log; 34 | 35 | public class BillingService extends Service implements ServiceConnection { 36 | 37 | private static enum Action { 38 | CHECK_BILLING_SUPPORTED, CHECK_SUBSCRIPTION_SUPPORTED, CONFIRM_NOTIFICATIONS, GET_PURCHASE_INFORMATION, REQUEST_PURCHASE, REQUEST_SUBSCRIPTION, RESTORE_TRANSACTIONS 39 | } 40 | 41 | private static final String ACTION_MARKET_BILLING_SERVICE = "com.android.vending.billing.MarketBillingService.BIND"; 42 | private static final String EXTRA_DEVELOPER_PAYLOAD = "DEVELOPER_PAYLOAD"; 43 | private static final String EXTRA_ITEM_ID = "ITEM_ID"; 44 | private static final String EXTRA_NONCE = "EXTRA_NONCE"; 45 | private static final String EXTRA_NOTIFY_IDS = "NOTIFY_IDS"; 46 | private static LinkedList mPendingRequests = new LinkedList(); 47 | 48 | private static IMarketBillingService mService; 49 | 50 | public static void checkBillingSupported(Context context) { 51 | final Intent intent = createIntent(context, Action.CHECK_BILLING_SUPPORTED); 52 | context.startService(intent); 53 | } 54 | 55 | public static void checkSubscriptionSupported(Context context) { 56 | final Intent intent = createIntent(context, Action.CHECK_SUBSCRIPTION_SUPPORTED); 57 | context.startService(intent); 58 | } 59 | 60 | public static void confirmNotifications(Context context, String[] notifyIds) { 61 | final Intent intent = createIntent(context, Action.CONFIRM_NOTIFICATIONS); 62 | intent.putExtra(EXTRA_NOTIFY_IDS, notifyIds); 63 | context.startService(intent); 64 | } 65 | 66 | private static Intent createIntent(Context context, Action action) { 67 | final String actionString = getActionForIntent(context, action); 68 | final Intent intent = new Intent(actionString); 69 | intent.setClass(context, BillingService.class); 70 | return intent; 71 | } 72 | 73 | private static final String getActionForIntent(Context context, Action action) { 74 | return context.getPackageName() + "." + action.toString(); 75 | } 76 | 77 | public static void getPurchaseInformation(Context context, String[] notifyIds, long nonce) { 78 | final Intent intent = createIntent(context, Action.GET_PURCHASE_INFORMATION); 79 | intent.putExtra(EXTRA_NOTIFY_IDS, notifyIds); 80 | intent.putExtra(EXTRA_NONCE, nonce); 81 | context.startService(intent); 82 | } 83 | 84 | public static void requestPurchase(Context context, String itemId, String developerPayload) { 85 | final Intent intent = createIntent(context, Action.REQUEST_PURCHASE); 86 | intent.putExtra(EXTRA_ITEM_ID, itemId); 87 | intent.putExtra(EXTRA_DEVELOPER_PAYLOAD, developerPayload); 88 | context.startService(intent); 89 | } 90 | 91 | public static void requestSubscription(Context context, String itemId, String developerPayload) { 92 | final Intent intent = createIntent(context, Action.REQUEST_SUBSCRIPTION); 93 | intent.putExtra(EXTRA_ITEM_ID, itemId); 94 | intent.putExtra(EXTRA_DEVELOPER_PAYLOAD, developerPayload); 95 | context.startService(intent); 96 | } 97 | 98 | public static void restoreTransations(Context context, long nonce) { 99 | final Intent intent = createIntent(context, Action.RESTORE_TRANSACTIONS); 100 | intent.setClass(context, BillingService.class); 101 | intent.putExtra(EXTRA_NONCE, nonce); 102 | context.startService(intent); 103 | } 104 | 105 | private void bindMarketBillingService() { 106 | try { 107 | final boolean bindResult = bindService(new Intent(ACTION_MARKET_BILLING_SERVICE), this, Context.BIND_AUTO_CREATE); 108 | if (!bindResult) { 109 | Log.e(this.getClass().getSimpleName(), "Could not bind to MarketBillingService"); 110 | } 111 | } catch (SecurityException e) { 112 | Log.e(this.getClass().getSimpleName(), "Could not bind to MarketBillingService", e); 113 | } 114 | } 115 | 116 | private void checkBillingSupported(int startId) { 117 | final String packageName = getPackageName(); 118 | final CheckBillingSupported request = new CheckBillingSupported(packageName, startId); 119 | runRequestOrQueue(request); 120 | } 121 | 122 | private void checkSubscriptionSupported(int startId) { 123 | final String packageName = getPackageName(); 124 | final CheckSubscriptionSupported request = new CheckSubscriptionSupported(packageName, startId); 125 | runRequestOrQueue(request); 126 | } 127 | 128 | private void confirmNotifications(Intent intent, int startId) { 129 | final String packageName = getPackageName(); 130 | final String[] notifyIds = intent.getStringArrayExtra(EXTRA_NOTIFY_IDS); 131 | final ConfirmNotifications request = new ConfirmNotifications(packageName, startId, notifyIds); 132 | runRequestOrQueue(request); 133 | } 134 | 135 | private Action getActionFromIntent(Intent intent) { 136 | final String actionString = intent.getAction(); 137 | if (actionString == null) { 138 | return null; 139 | } 140 | final String[] split = actionString.split("\\."); 141 | if (split.length <= 0) { 142 | return null; 143 | } 144 | return Action.valueOf(split[split.length - 1]); 145 | } 146 | 147 | private void getPurchaseInformation(Intent intent, int startId) { 148 | final String packageName = getPackageName(); 149 | final long nonce = intent.getLongExtra(EXTRA_NONCE, 0); 150 | final String[] notifyIds = intent.getStringArrayExtra(EXTRA_NOTIFY_IDS); 151 | final GetPurchaseInformation request = new GetPurchaseInformation(packageName, startId, notifyIds); 152 | request.setNonce(nonce); 153 | runRequestOrQueue(request); 154 | } 155 | 156 | @Override 157 | public IBinder onBind(Intent intent) { 158 | return null; 159 | } 160 | 161 | public void onServiceConnected(ComponentName name, IBinder service) { 162 | mService = IMarketBillingService.Stub.asInterface(service); 163 | runPendingRequests(); 164 | } 165 | 166 | public void onServiceDisconnected(ComponentName name) { 167 | mService = null; 168 | } 169 | 170 | // This is the old onStart method that will be called on the pre-2.0 171 | // platform. On 2.0 or later we override onStartCommand() so this 172 | // method will not be called. 173 | @Override 174 | public void onStart(Intent intent, int startId) { 175 | handleCommand(intent, startId); 176 | } 177 | 178 | // @Override // Avoid compile errors on pre-2.0 179 | public int onStartCommand(Intent intent, int flags, int startId) { 180 | handleCommand(intent, startId); 181 | return Compatibility.START_NOT_STICKY; 182 | } 183 | 184 | private void handleCommand(Intent intent, int startId) { 185 | final Action action = getActionFromIntent(intent); 186 | if (action == null) { 187 | return; 188 | } 189 | switch (action) { 190 | case CHECK_BILLING_SUPPORTED: 191 | checkBillingSupported(startId); 192 | break; 193 | case CHECK_SUBSCRIPTION_SUPPORTED: 194 | checkSubscriptionSupported(startId); 195 | break; 196 | case REQUEST_PURCHASE: 197 | requestPurchase(intent, startId); 198 | break; 199 | case REQUEST_SUBSCRIPTION: 200 | requestSubscription(intent, startId); 201 | break; 202 | case GET_PURCHASE_INFORMATION: 203 | getPurchaseInformation(intent, startId); 204 | break; 205 | case CONFIRM_NOTIFICATIONS: 206 | confirmNotifications(intent, startId); 207 | break; 208 | case RESTORE_TRANSACTIONS: 209 | restoreTransactions(intent, startId); 210 | } 211 | } 212 | 213 | private void requestPurchase(Intent intent, int startId) { 214 | final String packageName = getPackageName(); 215 | final String itemId = intent.getStringExtra(EXTRA_ITEM_ID); 216 | final String developerPayload = intent.getStringExtra(EXTRA_DEVELOPER_PAYLOAD); 217 | final RequestPurchase request = new RequestPurchase(packageName, startId, itemId, developerPayload); 218 | runRequestOrQueue(request); 219 | } 220 | 221 | private void requestSubscription(Intent intent, int startId) { 222 | final String packageName = getPackageName(); 223 | final String itemId = intent.getStringExtra(EXTRA_ITEM_ID); 224 | final String developerPayload = intent.getStringExtra(EXTRA_DEVELOPER_PAYLOAD); 225 | final RequestPurchase request = new RequestSubscription(packageName, startId, itemId, developerPayload); 226 | runRequestOrQueue(request); 227 | } 228 | 229 | private void restoreTransactions(Intent intent, int startId) { 230 | final String packageName = getPackageName(); 231 | final long nonce = intent.getLongExtra(EXTRA_NONCE, 0); 232 | final RestoreTransactions request = new RestoreTransactions(packageName, startId); 233 | request.setNonce(nonce); 234 | runRequestOrQueue(request); 235 | } 236 | 237 | private void runPendingRequests() { 238 | BillingRequest request; 239 | int maxStartId = -1; 240 | while ((request = mPendingRequests.peek()) != null) { 241 | if (mService != null) { 242 | runRequest(request); 243 | mPendingRequests.remove(); 244 | if (maxStartId < request.getStartId()) { 245 | maxStartId = request.getStartId(); 246 | } 247 | } else { 248 | bindMarketBillingService(); 249 | return; 250 | } 251 | } 252 | if (maxStartId >= 0) { 253 | stopSelf(maxStartId); 254 | } 255 | } 256 | 257 | private void runRequest(BillingRequest request) { 258 | try { 259 | final long requestId = request.run(mService); 260 | BillingController.onRequestSent(requestId, request); 261 | } catch (RemoteException e) { 262 | Log.w(this.getClass().getSimpleName(), "Remote billing service crashed"); 263 | // TODO: Retry? 264 | } 265 | } 266 | 267 | private void runRequestOrQueue(BillingRequest request) { 268 | mPendingRequests.add(request); 269 | if (mService == null) { 270 | bindMarketBillingService(); 271 | } else { 272 | runPendingRequests(); 273 | } 274 | } 275 | 276 | @Override 277 | public void onDestroy() { 278 | super.onDestroy(); 279 | // Ensure we're not leaking Android Market billing service 280 | if (mService != null) { 281 | try { 282 | unbindService(this); 283 | } catch (IllegalArgumentException e) { 284 | // This might happen if the service was disconnected 285 | } 286 | } 287 | } 288 | 289 | } 290 | -------------------------------------------------------------------------------- /AndroidBillingLibrary/src/net/robotmedia/billing/IBillingObserver.java: -------------------------------------------------------------------------------- 1 | /* Copyright 2011 Robot Media SL (http://www.robotmedia.net) 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 net.robotmedia.billing; 17 | 18 | import net.robotmedia.billing.BillingRequest.ResponseCode; 19 | import net.robotmedia.billing.model.Transaction.PurchaseState; 20 | import android.app.PendingIntent; 21 | 22 | public interface IBillingObserver { 23 | 24 | /** 25 | * Called after checking if in-app product billing is supported or not. 26 | * 27 | * @param supported 28 | * if true, in-app product billing is supported. If false, in-app 29 | * product billing is not supported, and neither is subscription 30 | * billing. 31 | * @see BillingController#checkBillingSupported(android.content.Context) 32 | */ 33 | public void onBillingChecked(boolean supported); 34 | 35 | /** 36 | * Called after checking if subscription billing is supported or not. 37 | * 38 | * @param supported 39 | * if true, subscription billing is supported, and also is in-app 40 | * product billing. Otherwise, subscription billing is not 41 | * supported. 42 | */ 43 | public void onSubscriptionChecked(boolean supported); 44 | 45 | /** 46 | * Called after requesting the purchase of the specified item. 47 | * 48 | * @param itemId 49 | * id of the item whose purchase was requested. 50 | * @param purchaseIntent 51 | * a purchase pending intent for the specified item. 52 | * @see BillingController#requestPurchase(android.content.Context, String, 53 | * boolean) 54 | */ 55 | public void onPurchaseIntent(String itemId, PendingIntent purchaseIntent); 56 | 57 | /** 58 | * Called when the specified item is purchased, cancelled or refunded. 59 | * 60 | * @param itemId 61 | * id of the item whose purchase state has changed. 62 | * @param state 63 | * purchase state of the specified item. 64 | */ 65 | public void onPurchaseStateChanged(String itemId, PurchaseState state); 66 | 67 | /** 68 | * Called with the response for the purchase request of the specified item. 69 | * This is used for reporting various errors, or if the user backed out and 70 | * didn't purchase the item. 71 | * 72 | * @param itemId 73 | * id of the item whose purchase was requested 74 | * @param response 75 | * response of the purchase request 76 | */ 77 | public void onRequestPurchaseResponse(String itemId, ResponseCode response); 78 | 79 | /** 80 | * Called when a restore transactions request has been successfully 81 | * received by the server. 82 | */ 83 | public void onTransactionsRestored(); 84 | 85 | } 86 | -------------------------------------------------------------------------------- /AndroidBillingLibrary/src/net/robotmedia/billing/helper/AbstractBillingActivity.java: -------------------------------------------------------------------------------- 1 | /* Copyright 2011 Robot Media SL (http://www.robotmedia.net) 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 net.robotmedia.billing.helper; 17 | 18 | import net.robotmedia.billing.BillingController; 19 | import net.robotmedia.billing.BillingController.BillingStatus; 20 | import net.robotmedia.billing.BillingRequest.ResponseCode; 21 | import net.robotmedia.billing.model.Transaction.PurchaseState; 22 | import android.app.Activity; 23 | 24 | public abstract class AbstractBillingActivity extends Activity implements BillingController.IConfiguration { 25 | 26 | protected AbstractBillingObserver mBillingObserver; 27 | 28 | /** 29 | *

30 | * Returns the in-app product billing support status, and checks it 31 | * asynchronously if it is currently unknown. 32 | * {@link AbstractBillingActivity#onBillingChecked(boolean)} will be called 33 | * eventually with the result. 34 | *

35 | *

36 | * In-app product support does not imply subscription support. To check if 37 | * subscriptions are supported, use 38 | * {@link AbstractBillingActivity#checkSubscriptionSupported()}. 39 | *

40 | * 41 | * @return the current in-app product billing support status (unknown, 42 | * supported or unsupported). If it is unsupported, subscriptions 43 | * are also unsupported. 44 | * @see AbstractBillingActivity#onBillingChecked(boolean) 45 | * @see AbstractBillingActivity#checkSubscriptionSupported() 46 | */ 47 | public BillingStatus checkBillingSupported() { 48 | return BillingController.checkBillingSupported(this); 49 | } 50 | 51 | /** 52 | *

53 | * Returns the subscription billing support status, and checks it 54 | * asynchronously if it is currently unknown. 55 | * {@link AbstractBillingActivity#onSubscriptionChecked(boolean)} will be 56 | * called eventually with the result. 57 | *

58 | *

59 | * No support for subscriptions does not imply that in-app products are also 60 | * unsupported. To check if subscriptions are supported, use 61 | * {@link AbstractBillingActivity#checkSubscriptionSupported()}. 62 | *

63 | * 64 | * @return the current in-app product billing support status (unknown, 65 | * supported or unsupported). If it is unsupported, subscriptions 66 | * are also unsupported. 67 | * @see AbstractBillingActivity#onBillingChecked(boolean) 68 | * @see AbstractBillingActivity#checkSubscriptionSupported() 69 | */ 70 | public BillingStatus checkSubscriptionSupported() { 71 | return BillingController.checkSubscriptionSupported(this); 72 | } 73 | 74 | public abstract void onBillingChecked(boolean supported); 75 | 76 | public abstract void onSubscriptionChecked(boolean supported); 77 | 78 | @Override 79 | protected void onCreate(android.os.Bundle savedInstanceState) { 80 | super.onCreate(savedInstanceState); 81 | 82 | mBillingObserver = new AbstractBillingObserver(this) { 83 | 84 | public void onBillingChecked(boolean supported) { 85 | AbstractBillingActivity.this.onBillingChecked(supported); 86 | } 87 | 88 | public void onSubscriptionChecked(boolean supported) { 89 | AbstractBillingActivity.this.onSubscriptionChecked(supported); 90 | } 91 | 92 | public void onPurchaseStateChanged(String itemId, PurchaseState state) { 93 | AbstractBillingActivity.this.onPurchaseStateChanged(itemId, state); 94 | } 95 | 96 | public void onRequestPurchaseResponse(String itemId, ResponseCode response) { 97 | AbstractBillingActivity.this.onRequestPurchaseResponse(itemId, response); 98 | } 99 | }; 100 | BillingController.registerObserver(mBillingObserver); 101 | BillingController.setConfiguration(this); // This activity will provide 102 | // the public key and salt 103 | this.checkBillingSupported(); 104 | if (!mBillingObserver.isTransactionsRestored()) { 105 | BillingController.restoreTransactions(this); 106 | } 107 | } 108 | 109 | @Override 110 | protected void onDestroy() { 111 | super.onDestroy(); 112 | BillingController.unregisterObserver(mBillingObserver); // Avoid 113 | // receiving 114 | // notifications after 115 | // destroy 116 | BillingController.setConfiguration(null); 117 | } 118 | 119 | public abstract void onPurchaseStateChanged(String itemId, PurchaseState state);; 120 | 121 | public abstract void onRequestPurchaseResponse(String itemId, ResponseCode response); 122 | 123 | /** 124 | * Requests the purchase of the specified item. The transaction will not be 125 | * confirmed automatically; such confirmation could be handled in 126 | * {@link AbstractBillingActivity#onPurchaseExecuted(String)}. If automatic 127 | * confirmation is preferred use 128 | * {@link BillingController#requestPurchase(android.content.Context, String, boolean)} 129 | * instead. 130 | * 131 | * @param itemId 132 | * id of the item to be purchased. 133 | */ 134 | public void requestPurchase(String itemId) { 135 | BillingController.requestPurchase(this, itemId); 136 | } 137 | 138 | /** 139 | * Requests the purchase of the specified subscription item. The transaction 140 | * will not be confirmed automatically; such confirmation could be handled 141 | * in {@link AbstractBillingActivity#onPurchaseExecuted(String)}. If 142 | * automatic confirmation is preferred use 143 | * {@link BillingController#requestPurchase(android.content.Context, String, boolean)} 144 | * instead. 145 | * 146 | * @param itemId 147 | * id of the item to be purchased. 148 | */ 149 | public void requestSubscription(String itemId) { 150 | BillingController.requestSubscription(this, itemId); 151 | } 152 | 153 | /** 154 | * Requests to restore all transactions. 155 | */ 156 | public void restoreTransactions() { 157 | BillingController.restoreTransactions(this); 158 | } 159 | 160 | } 161 | -------------------------------------------------------------------------------- /AndroidBillingLibrary/src/net/robotmedia/billing/helper/AbstractBillingFragment.java: -------------------------------------------------------------------------------- 1 | package net.robotmedia.billing.helper; 2 | 3 | import net.robotmedia.billing.BillingController; 4 | import net.robotmedia.billing.BillingController.BillingStatus; 5 | import net.robotmedia.billing.BillingRequest.ResponseCode; 6 | import net.robotmedia.billing.model.Transaction.PurchaseState; 7 | import android.annotation.TargetApi; 8 | import android.app.Fragment; 9 | 10 | @TargetApi(11) 11 | public abstract class AbstractBillingFragment extends Fragment implements BillingController.IConfiguration { 12 | 13 | protected AbstractBillingObserver mBillingObserver; 14 | 15 | /** 16 | *

17 | * Returns the in-app product billing support status, and checks it 18 | * asynchronously if it is currently unknown. 19 | * {@link AbstractBillingActivity#onBillingChecked(boolean)} will be called 20 | * eventually with the result. 21 | *

22 | *

23 | * In-app product support does not imply subscription support. To check if 24 | * subscriptions are supported, use 25 | * {@link AbstractBillingActivity#checkSubscriptionSupported()}. 26 | *

27 | * 28 | * @return the current in-app product billing support status (unknown, 29 | * supported or unsupported). If it is unsupported, subscriptions 30 | * are also unsupported. 31 | * @see AbstractBillingActivity#onBillingChecked(boolean) 32 | * @see AbstractBillingActivity#checkSubscriptionSupported() 33 | */ 34 | public BillingStatus checkBillingSupported() { 35 | return BillingController.checkBillingSupported(getActivity()); 36 | } 37 | 38 | /** 39 | *

40 | * Returns the subscription billing support status, and checks it 41 | * asynchronously if it is currently unknown. 42 | * {@link AbstractBillingActivity#onSubscriptionChecked(boolean)} will be 43 | * called eventually with the result. 44 | *

45 | *

46 | * No support for subscriptions does not imply that in-app products are also 47 | * unsupported. To check if subscriptions are supported, use 48 | * {@link AbstractBillingActivity#checkSubscriptionSupported()}. 49 | *

50 | * 51 | * @return the current in-app product billing support status (unknown, 52 | * supported or unsupported). If it is unsupported, subscriptions 53 | * are also unsupported. 54 | * @see AbstractBillingActivity#onBillingChecked(boolean) 55 | * @see AbstractBillingActivity#checkSubscriptionSupported() 56 | */ 57 | public BillingStatus checkSubscriptionSupported() { 58 | return BillingController.checkSubscriptionSupported(getActivity()); 59 | } 60 | 61 | public abstract void onBillingChecked(boolean supported); 62 | 63 | public abstract void onSubscriptionChecked(boolean supported); 64 | 65 | @Override 66 | public void onCreate(android.os.Bundle savedInstanceState) { 67 | super.onCreate(savedInstanceState); 68 | 69 | mBillingObserver = new AbstractBillingObserver(getActivity()) { 70 | 71 | public void onBillingChecked(boolean supported) { 72 | AbstractBillingFragment.this.onBillingChecked(supported); 73 | } 74 | 75 | public void onSubscriptionChecked(boolean supported) { 76 | AbstractBillingFragment.this.onSubscriptionChecked(supported); 77 | } 78 | 79 | public void onPurchaseStateChanged(String itemId, PurchaseState state) { 80 | AbstractBillingFragment.this.onPurchaseStateChanged(itemId, state); 81 | } 82 | 83 | public void onRequestPurchaseResponse(String itemId, ResponseCode response) { 84 | AbstractBillingFragment.this.onRequestPurchaseResponse(itemId, response); 85 | } 86 | }; 87 | BillingController.registerObserver(mBillingObserver); 88 | BillingController.setConfiguration(this); // This fragment will provide 89 | // the public key and salt 90 | this.checkBillingSupported(); 91 | if (!mBillingObserver.isTransactionsRestored()) { 92 | BillingController.restoreTransactions(getActivity()); 93 | } 94 | } 95 | 96 | @Override 97 | public void onDestroy() { 98 | super.onDestroy(); 99 | BillingController.unregisterObserver(mBillingObserver); // Avoid 100 | // receiving 101 | // notifications 102 | // after destroy 103 | BillingController.setConfiguration(null); 104 | } 105 | 106 | public abstract void onPurchaseStateChanged(String itemId, PurchaseState state);; 107 | 108 | public abstract void onRequestPurchaseResponse(String itemId, ResponseCode response); 109 | 110 | /** 111 | * Requests the purchase of the specified item. The transaction will not be 112 | * confirmed automatically; such confirmation could be handled in 113 | * {@link AbstractBillingActivity#onPurchaseExecuted(String)}. If automatic 114 | * confirmation is preferred use 115 | * {@link BillingController#requestPurchase(android.content.Context, String, boolean)} 116 | * instead. 117 | * 118 | * @param itemId 119 | * id of the item to be purchased. 120 | */ 121 | public void requestPurchase(String itemId) { 122 | BillingController.requestPurchase(getActivity(), itemId); 123 | } 124 | 125 | /** 126 | * Requests the purchase of the specified subscription item. The transaction 127 | * will not be confirmed automatically; such confirmation could be handled 128 | * in {@link AbstractBillingActivity#onPurchaseExecuted(String)}. If 129 | * automatic confirmation is preferred use 130 | * {@link BillingController#requestPurchase(android.content.Context, String, boolean)} 131 | * instead. 132 | * 133 | * @param itemId 134 | * id of the item to be purchased. 135 | */ 136 | public void requestSubscription(String itemId) { 137 | BillingController.requestSubscription(getActivity(), itemId); 138 | } 139 | 140 | /** 141 | * Requests to restore all transactions. 142 | */ 143 | public void restoreTransactions() { 144 | BillingController.restoreTransactions(getActivity()); 145 | } 146 | 147 | } 148 | -------------------------------------------------------------------------------- /AndroidBillingLibrary/src/net/robotmedia/billing/helper/AbstractBillingObserver.java: -------------------------------------------------------------------------------- 1 | /* Copyright 2011 Robot Media SL (http://www.robotmedia.net) 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 net.robotmedia.billing.helper; 17 | 18 | import android.app.Activity; 19 | import android.app.PendingIntent; 20 | import android.content.SharedPreferences; 21 | import android.content.SharedPreferences.Editor; 22 | import android.preference.PreferenceManager; 23 | import net.robotmedia.billing.BillingController; 24 | import net.robotmedia.billing.IBillingObserver; 25 | 26 | /** 27 | * Abstract subclass of IBillingObserver that provides default implementations 28 | * for {@link IBillingObserver#onPurchaseIntent(String, PendingIntent)} and 29 | * {@link IBillingObserver#onTransactionsRestored()}. 30 | * 31 | */ 32 | public abstract class AbstractBillingObserver implements IBillingObserver { 33 | 34 | protected static final String KEY_TRANSACTIONS_RESTORED = "net.robotmedia.billing.transactionsRestored"; 35 | 36 | protected Activity activity; 37 | 38 | public AbstractBillingObserver(Activity activity) { 39 | this.activity = activity; 40 | } 41 | 42 | public boolean isTransactionsRestored() { 43 | final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity); 44 | return preferences.getBoolean(KEY_TRANSACTIONS_RESTORED, false); 45 | } 46 | 47 | /** 48 | * Called after requesting the purchase of the specified item. The default 49 | * implementation simply starts the pending intent. 50 | * 51 | * @param itemId 52 | * id of the item whose purchase was requested. 53 | * @param purchaseIntent 54 | * a purchase pending intent for the specified item. 55 | */ 56 | public void onPurchaseIntent(String itemId, PendingIntent purchaseIntent) { 57 | BillingController.startPurchaseIntent(activity, purchaseIntent, null); 58 | } 59 | 60 | public void onTransactionsRestored() { 61 | final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity); 62 | final Editor editor = preferences.edit(); 63 | editor.putBoolean(KEY_TRANSACTIONS_RESTORED, true); 64 | editor.commit(); 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /AndroidBillingLibrary/src/net/robotmedia/billing/model/BillingDB.java: -------------------------------------------------------------------------------- 1 | /* Copyright 2011 Robot Media SL (http://www.robotmedia.net) 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 net.robotmedia.billing.model; 17 | 18 | import net.robotmedia.billing.model.Transaction.PurchaseState; 19 | import android.content.ContentValues; 20 | import android.content.Context; 21 | import android.database.Cursor; 22 | import android.database.sqlite.SQLiteDatabase; 23 | import android.database.sqlite.SQLiteOpenHelper; 24 | 25 | public class BillingDB { 26 | static final String DATABASE_NAME = "billing.db"; 27 | static final int DATABASE_VERSION = 1; 28 | static final String TABLE_TRANSACTIONS = "purchases"; 29 | 30 | public static final String COLUMN__ID = "_id"; 31 | public static final String COLUMN_STATE = "state"; 32 | public static final String COLUMN_PRODUCT_ID = "productId"; 33 | public static final String COLUMN_PURCHASE_TIME = "purchaseTime"; 34 | public static final String COLUMN_DEVELOPER_PAYLOAD = "developerPayload"; 35 | 36 | private static final String[] TABLE_TRANSACTIONS_COLUMNS = { 37 | COLUMN__ID, COLUMN_PRODUCT_ID, COLUMN_STATE, 38 | COLUMN_PURCHASE_TIME, COLUMN_DEVELOPER_PAYLOAD 39 | }; 40 | 41 | SQLiteDatabase mDb; 42 | private DatabaseHelper mDatabaseHelper; 43 | 44 | public BillingDB(Context context) { 45 | mDatabaseHelper = new DatabaseHelper(context); 46 | mDb = mDatabaseHelper.getWritableDatabase(); 47 | } 48 | 49 | public void close() { 50 | mDatabaseHelper.close(); 51 | } 52 | 53 | public void insert(Transaction transaction) { 54 | ContentValues values = new ContentValues(); 55 | values.put(COLUMN__ID, transaction.orderId); 56 | values.put(COLUMN_PRODUCT_ID, transaction.productId); 57 | values.put(COLUMN_STATE, transaction.purchaseState.ordinal()); 58 | values.put(COLUMN_PURCHASE_TIME, transaction.purchaseTime); 59 | values.put(COLUMN_DEVELOPER_PAYLOAD, transaction.developerPayload); 60 | mDb.replace(TABLE_TRANSACTIONS, null /* nullColumnHack */, values); 61 | } 62 | 63 | public Cursor queryTransactions() { 64 | return mDb.query(TABLE_TRANSACTIONS, TABLE_TRANSACTIONS_COLUMNS, null, 65 | null, null, null, null); 66 | } 67 | 68 | public Cursor queryTransactions(String productId) { 69 | return mDb.query(TABLE_TRANSACTIONS, TABLE_TRANSACTIONS_COLUMNS, COLUMN_PRODUCT_ID + " = ?", 70 | new String[] {productId}, null, null, null); 71 | } 72 | 73 | public Cursor queryTransactions(String productId, PurchaseState state) { 74 | return mDb.query(TABLE_TRANSACTIONS, TABLE_TRANSACTIONS_COLUMNS, COLUMN_PRODUCT_ID + " = ? AND " + COLUMN_STATE + " = ?", 75 | new String[] {productId, String.valueOf(state.ordinal())}, null, null, null); 76 | } 77 | 78 | protected static final Transaction createTransaction(Cursor cursor) { 79 | final Transaction purchase = new Transaction(); 80 | purchase.orderId = cursor.getString(0); 81 | purchase.productId = cursor.getString(1); 82 | purchase.purchaseState = PurchaseState.valueOf(cursor.getInt(2)); 83 | purchase.purchaseTime = cursor.getLong(3); 84 | purchase.developerPayload = cursor.getString(4); 85 | return purchase; 86 | } 87 | 88 | private class DatabaseHelper extends SQLiteOpenHelper { 89 | public DatabaseHelper(Context context) { 90 | super(context, DATABASE_NAME, null, DATABASE_VERSION); 91 | } 92 | 93 | @Override 94 | public void onCreate(SQLiteDatabase db) { 95 | createTransactionsTable(db); 96 | } 97 | 98 | private void createTransactionsTable(SQLiteDatabase db) { 99 | db.execSQL("CREATE TABLE " + TABLE_TRANSACTIONS + "(" + 100 | COLUMN__ID + " TEXT PRIMARY KEY, " + 101 | COLUMN_PRODUCT_ID + " INTEGER, " + 102 | COLUMN_STATE + " TEXT, " + 103 | COLUMN_PURCHASE_TIME + " TEXT, " + 104 | COLUMN_DEVELOPER_PAYLOAD + " INTEGER)"); 105 | } 106 | 107 | @Override 108 | public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {} 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /AndroidBillingLibrary/src/net/robotmedia/billing/model/Transaction.java: -------------------------------------------------------------------------------- 1 | /* Copyright 2011 Robot Media SL (http://www.robotmedia.net) 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 net.robotmedia.billing.model; 17 | 18 | import org.json.JSONException; 19 | import org.json.JSONObject; 20 | 21 | public class Transaction { 22 | 23 | public enum PurchaseState { 24 | // Responses to requestPurchase or restoreTransactions. 25 | PURCHASED, // 0: User was charged for the order. 26 | CANCELLED, // 1: The charge failed on the server. 27 | REFUNDED, // 2: User received a refund for the order. 28 | EXPIRED; // 3: Sent at the end of a billing cycle to indicate that the 29 | // subscription expired without renewal because of 30 | // non-payment or user-cancellation. Your app does not need 31 | // to grant continued access to the subscription content. 32 | 33 | // Converts from an ordinal value to the PurchaseState 34 | public static PurchaseState valueOf(int index) { 35 | PurchaseState[] values = PurchaseState.values(); 36 | if (index < 0 || index >= values.length) { 37 | return CANCELLED; 38 | } 39 | return values[index]; 40 | } 41 | } 42 | static final String DEVELOPER_PAYLOAD = "developerPayload"; 43 | static final String NOTIFICATION_ID = "notificationId"; 44 | static final String ORDER_ID = "orderId"; 45 | static final String PACKAGE_NAME = "packageName"; 46 | static final String PRODUCT_ID = "productId"; 47 | static final String PURCHASE_STATE = "purchaseState"; 48 | 49 | static final String PURCHASE_TIME = "purchaseTime"; 50 | 51 | public static Transaction parse(JSONObject json) throws JSONException { 52 | final Transaction transaction = new Transaction(); 53 | final int response = json.getInt(PURCHASE_STATE); 54 | transaction.purchaseState = PurchaseState.valueOf(response); 55 | transaction.productId = json.getString(PRODUCT_ID); 56 | transaction.packageName = json.getString(PACKAGE_NAME); 57 | transaction.purchaseTime = json.getLong(PURCHASE_TIME); 58 | transaction.orderId = json.optString(ORDER_ID, null); 59 | transaction.notificationId = json.optString(NOTIFICATION_ID, null); 60 | transaction.developerPayload = json.optString(DEVELOPER_PAYLOAD, null); 61 | return transaction; 62 | } 63 | 64 | public String developerPayload; 65 | public String notificationId; 66 | public String orderId; 67 | public String packageName; 68 | public String productId; 69 | public PurchaseState purchaseState; 70 | public long purchaseTime; 71 | 72 | public Transaction() {} 73 | 74 | public Transaction(String orderId, String productId, String packageName, PurchaseState purchaseState, 75 | String notificationId, long purchaseTime, String developerPayload) { 76 | this.orderId = orderId; 77 | this.productId = productId; 78 | this.packageName = packageName; 79 | this.purchaseState = purchaseState; 80 | this.notificationId = notificationId; 81 | this.purchaseTime = purchaseTime; 82 | this.developerPayload = developerPayload; 83 | } 84 | 85 | public Transaction clone() { 86 | return new Transaction(orderId, productId, packageName, purchaseState, notificationId, purchaseTime, developerPayload); 87 | } 88 | 89 | @Override 90 | public boolean equals(Object obj) { 91 | if (this == obj) 92 | return true; 93 | if (obj == null) 94 | return false; 95 | if (getClass() != obj.getClass()) 96 | return false; 97 | Transaction other = (Transaction) obj; 98 | if (developerPayload == null) { 99 | if (other.developerPayload != null) 100 | return false; 101 | } else if (!developerPayload.equals(other.developerPayload)) 102 | return false; 103 | if (notificationId == null) { 104 | if (other.notificationId != null) 105 | return false; 106 | } else if (!notificationId.equals(other.notificationId)) 107 | return false; 108 | if (orderId == null) { 109 | if (other.orderId != null) 110 | return false; 111 | } else if (!orderId.equals(other.orderId)) 112 | return false; 113 | if (packageName == null) { 114 | if (other.packageName != null) 115 | return false; 116 | } else if (!packageName.equals(other.packageName)) 117 | return false; 118 | if (productId == null) { 119 | if (other.productId != null) 120 | return false; 121 | } else if (!productId.equals(other.productId)) 122 | return false; 123 | if (purchaseState != other.purchaseState) 124 | return false; 125 | if (purchaseTime != other.purchaseTime) 126 | return false; 127 | return true; 128 | } 129 | 130 | @Override 131 | public String toString() { 132 | return String.valueOf(orderId); 133 | } 134 | 135 | } 136 | -------------------------------------------------------------------------------- /AndroidBillingLibrary/src/net/robotmedia/billing/model/TransactionManager.java: -------------------------------------------------------------------------------- 1 | /* Copyright 2011 Robot Media SL (http://www.robotmedia.net) 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 net.robotmedia.billing.model; 17 | 18 | import java.util.ArrayList; 19 | import java.util.List; 20 | 21 | import net.robotmedia.billing.model.Transaction.PurchaseState; 22 | import android.content.Context; 23 | import android.database.Cursor; 24 | 25 | public class TransactionManager { 26 | 27 | public synchronized static void addTransaction(Context context, Transaction transaction) { 28 | BillingDB db = new BillingDB(context); 29 | db.insert(transaction); 30 | db.close(); 31 | } 32 | 33 | public synchronized static boolean isPurchased(Context context, String itemId) { 34 | return countPurchases(context, itemId) > 0; 35 | } 36 | 37 | public synchronized static int countPurchases(Context context, String itemId) { 38 | BillingDB db = new BillingDB(context); 39 | final Cursor c = db.queryTransactions(itemId, PurchaseState.PURCHASED); 40 | int count = 0; 41 | if (c != null) { 42 | count = c.getCount(); 43 | c.close(); 44 | } 45 | db.close(); 46 | return count; 47 | } 48 | 49 | public synchronized static List getTransactions(Context context) { 50 | BillingDB db = new BillingDB(context); 51 | final Cursor c = db.queryTransactions(); 52 | final List transactions = cursorToList(c); 53 | db.close(); 54 | return transactions; 55 | } 56 | 57 | private static List cursorToList(final Cursor c) { 58 | final List transactions = new ArrayList(); 59 | if (c != null) { 60 | while (c.moveToNext()) { 61 | final Transaction purchase = BillingDB.createTransaction(c); 62 | transactions.add(purchase); 63 | } 64 | c.close(); 65 | } 66 | return transactions; 67 | } 68 | 69 | public synchronized static List getTransactions(Context context, String itemId) { 70 | BillingDB db = new BillingDB(context); 71 | final Cursor c = db.queryTransactions(itemId); 72 | final List transactions = cursorToList(c); 73 | db.close(); 74 | return transactions; 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /AndroidBillingLibrary/src/net/robotmedia/billing/security/DefaultSignatureValidator.java: -------------------------------------------------------------------------------- 1 | /* Copyright 2011 Robot Media SL (http://www.robotmedia.net) 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 net.robotmedia.billing.security; 17 | 18 | import java.security.InvalidKeyException; 19 | import java.security.KeyFactory; 20 | import java.security.NoSuchAlgorithmException; 21 | import java.security.PublicKey; 22 | import java.security.Signature; 23 | import java.security.SignatureException; 24 | import java.security.spec.InvalidKeySpecException; 25 | import java.security.spec.X509EncodedKeySpec; 26 | 27 | import android.text.TextUtils; 28 | import android.util.Log; 29 | import net.robotmedia.billing.BillingController; 30 | import net.robotmedia.billing.utils.Base64; 31 | import net.robotmedia.billing.utils.Base64DecoderException; 32 | 33 | public class DefaultSignatureValidator implements ISignatureValidator { 34 | 35 | protected static final String KEY_FACTORY_ALGORITHM = "RSA"; 36 | protected static final String SIGNATURE_ALGORITHM = "SHA1withRSA"; 37 | 38 | /** 39 | * Generates a PublicKey instance from a string containing the 40 | * Base64-encoded public key. 41 | * 42 | * @param encodedPublicKey 43 | * Base64-encoded public key 44 | * @throws IllegalArgumentException 45 | * if encodedPublicKey is invalid 46 | */ 47 | protected PublicKey generatePublicKey(String encodedPublicKey) { 48 | try { 49 | byte[] decodedKey = Base64.decode(encodedPublicKey); 50 | KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM); 51 | return keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey)); 52 | } catch (NoSuchAlgorithmException e) { 53 | throw new RuntimeException(e); 54 | } catch (InvalidKeySpecException e) { 55 | Log.e(BillingController.LOG_TAG, "Invalid key specification."); 56 | throw new IllegalArgumentException(e); 57 | } catch (Base64DecoderException e) { 58 | Log.e(BillingController.LOG_TAG, "Base64 decoding failed."); 59 | throw new IllegalArgumentException(e); 60 | } 61 | } 62 | 63 | private BillingController.IConfiguration configuration; 64 | 65 | public DefaultSignatureValidator(BillingController.IConfiguration configuration) { 66 | this.configuration = configuration; 67 | } 68 | 69 | protected boolean validate(PublicKey publicKey, String signedData, String signature) { 70 | Signature sig; 71 | try { 72 | sig = Signature.getInstance(SIGNATURE_ALGORITHM); 73 | sig.initVerify(publicKey); 74 | sig.update(signedData.getBytes()); 75 | if (!sig.verify(Base64.decode(signature))) { 76 | Log.e(BillingController.LOG_TAG, "Signature verification failed."); 77 | return false; 78 | } 79 | return true; 80 | } catch (NoSuchAlgorithmException e) { 81 | Log.e(BillingController.LOG_TAG, "NoSuchAlgorithmException"); 82 | } catch (InvalidKeyException e) { 83 | Log.e(BillingController.LOG_TAG, "Invalid key specification"); 84 | } catch (SignatureException e) { 85 | Log.e(BillingController.LOG_TAG, "Signature exception"); 86 | } catch (Base64DecoderException e) { 87 | Log.e(BillingController.LOG_TAG, "Base64 decoding failed"); 88 | } 89 | return false; 90 | } 91 | 92 | public boolean validate(String signedData, String signature) { 93 | final String publicKey; 94 | if (configuration == null || TextUtils.isEmpty(publicKey = configuration.getPublicKey())) { 95 | Log.w(BillingController.LOG_TAG, "Please set the public key or turn on debug mode"); 96 | return false; 97 | } 98 | if (signedData == null) { 99 | Log.e(BillingController.LOG_TAG, "Data is null"); 100 | return false; 101 | } 102 | PublicKey key = generatePublicKey(publicKey); 103 | return validate(key, signedData, signature); 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /AndroidBillingLibrary/src/net/robotmedia/billing/security/ISignatureValidator.java: -------------------------------------------------------------------------------- 1 | /* Copyright 2011 Robot Media SL (http://www.robotmedia.net) 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 net.robotmedia.billing.security; 17 | 18 | public interface ISignatureValidator { 19 | 20 | /** 21 | * Validates that the specified signature matches the computed signature on 22 | * the specified signed data. Returns true if the data is correctly signed. 23 | * 24 | * @param signedData 25 | * signed data 26 | * @param signature 27 | * signature 28 | * @return true if the data and signature match, false otherwise. 29 | */ 30 | public boolean validate(String signedData, String signature); 31 | 32 | } -------------------------------------------------------------------------------- /AndroidBillingLibrary/src/net/robotmedia/billing/utils/AESObfuscator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2010 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.robotmedia.billing.utils; 18 | 19 | import java.io.UnsupportedEncodingException; 20 | import java.security.GeneralSecurityException; 21 | import java.security.spec.KeySpec; 22 | 23 | import javax.crypto.BadPaddingException; 24 | import javax.crypto.Cipher; 25 | import javax.crypto.IllegalBlockSizeException; 26 | import javax.crypto.SecretKey; 27 | import javax.crypto.SecretKeyFactory; 28 | import javax.crypto.spec.IvParameterSpec; 29 | import javax.crypto.spec.PBEKeySpec; 30 | import javax.crypto.spec.SecretKeySpec; 31 | 32 | /** 33 | * An obfuscator that uses AES to encrypt data. 34 | */ 35 | public class AESObfuscator { 36 | private static final String UTF8 = "UTF-8"; 37 | private static final String KEYGEN_ALGORITHM = "PBEWITHSHAAND256BITAES-CBC-BC"; 38 | private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding"; 39 | private static final byte[] IV = 40 | { 16, 74, 71, -80, 32, 101, -47, 72, 117, -14, 0, -29, 70, 65, -12, 74 }; 41 | private static final String header = "net.robotmedia.billing.utils.AESObfuscator-1|"; 42 | 43 | private Cipher mEncryptor; 44 | private Cipher mDecryptor; 45 | 46 | public AESObfuscator(byte[] salt, String password) { 47 | try { 48 | SecretKeyFactory factory = SecretKeyFactory.getInstance(KEYGEN_ALGORITHM); 49 | KeySpec keySpec = 50 | new PBEKeySpec(password.toCharArray(), salt, 1024, 256); 51 | SecretKey tmp = factory.generateSecret(keySpec); 52 | SecretKey secret = new SecretKeySpec(tmp.getEncoded(), "AES"); 53 | mEncryptor = Cipher.getInstance(CIPHER_ALGORITHM); 54 | mEncryptor.init(Cipher.ENCRYPT_MODE, secret, new IvParameterSpec(IV)); 55 | mDecryptor = Cipher.getInstance(CIPHER_ALGORITHM); 56 | mDecryptor.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec(IV)); 57 | } catch (GeneralSecurityException e) { 58 | // This can't happen on a compatible Android device. 59 | throw new RuntimeException("Invalid environment", e); 60 | } 61 | } 62 | 63 | public String obfuscate(String original) { 64 | if (original == null) { 65 | return null; 66 | } 67 | try { 68 | // Header is appended as an integrity check 69 | return Base64.encode(mEncryptor.doFinal((header + original).getBytes(UTF8))); 70 | } catch (UnsupportedEncodingException e) { 71 | throw new RuntimeException("Invalid environment", e); 72 | } catch (GeneralSecurityException e) { 73 | throw new RuntimeException("Invalid environment", e); 74 | } 75 | } 76 | 77 | public String unobfuscate(String obfuscated) throws ValidationException { 78 | if (obfuscated == null) { 79 | return null; 80 | } 81 | try { 82 | String result = new String(mDecryptor.doFinal(Base64.decode(obfuscated)), UTF8); 83 | // Check for presence of header. This serves as a final integrity check, for cases 84 | // where the block size is correct during decryption. 85 | int headerIndex = result.indexOf(header); 86 | if (headerIndex != 0) { 87 | throw new ValidationException("Header not found (invalid data or key)" + ":" + 88 | obfuscated); 89 | } 90 | return result.substring(header.length(), result.length()); 91 | } catch (Base64DecoderException e) { 92 | throw new ValidationException(e.getMessage() + ":" + obfuscated); 93 | } catch (IllegalBlockSizeException e) { 94 | throw new ValidationException(e.getMessage() + ":" + obfuscated); 95 | } catch (BadPaddingException e) { 96 | throw new ValidationException(e.getMessage() + ":" + obfuscated); 97 | } catch (UnsupportedEncodingException e) { 98 | throw new RuntimeException("Invalid environment", e); 99 | } 100 | } 101 | 102 | public class ValidationException extends Exception { 103 | public ValidationException() { 104 | super(); 105 | } 106 | 107 | public ValidationException(String s) { 108 | super(s); 109 | } 110 | 111 | private static final long serialVersionUID = 1L; 112 | } 113 | 114 | } -------------------------------------------------------------------------------- /AndroidBillingLibrary/src/net/robotmedia/billing/utils/Base64.java: -------------------------------------------------------------------------------- 1 | // Portions copyright 2002, 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 | package net.robotmedia.billing.utils; 16 | 17 | // This code was converted from code at http://iharder.sourceforge.net/base64/ 18 | // Lots of extraneous features were removed. 19 | /* The original code said: 20 | *

21 | * I am placing this code in the Public Domain. Do with it as you will. 22 | * This software comes with no guarantees or warranties but with 23 | * plenty of well-wishing instead! 24 | * Please visit 25 | * http://iharder.net/xmlizable 26 | * periodically to check for updates or to contribute improvements. 27 | *

28 | * 29 | * @author Robert Harder 30 | * @author rharder@usa.net 31 | * @version 1.3 32 | */ 33 | 34 | /** 35 | * Base64 converter class. This code is not a complete MIME encoder; 36 | * it simply converts binary data to base64 data and back. 37 | * 38 | *

Note {@link CharBase64} is a GWT-compatible implementation of this 39 | * class. 40 | */ 41 | public class Base64 { 42 | /** Specify encoding (value is {@code true}). */ 43 | public final static boolean ENCODE = true; 44 | 45 | /** Specify decoding (value is {@code false}). */ 46 | public final static boolean DECODE = false; 47 | 48 | /** The equals sign (=) as a byte. */ 49 | private final static byte EQUALS_SIGN = (byte) '='; 50 | 51 | /** The new line character (\n) as a byte. */ 52 | private final static byte NEW_LINE = (byte) '\n'; 53 | 54 | /** 55 | * The 64 valid Base64 values. 56 | */ 57 | private final static byte[] ALPHABET = 58 | {(byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F', 59 | (byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K', 60 | (byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P', 61 | (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U', 62 | (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z', 63 | (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', 64 | (byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j', 65 | (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o', 66 | (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't', 67 | (byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y', 68 | (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3', 69 | (byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8', 70 | (byte) '9', (byte) '+', (byte) '/'}; 71 | 72 | /** 73 | * The 64 valid web safe Base64 values. 74 | */ 75 | private final static byte[] WEBSAFE_ALPHABET = 76 | {(byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F', 77 | (byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K', 78 | (byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P', 79 | (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U', 80 | (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z', 81 | (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', 82 | (byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j', 83 | (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o', 84 | (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't', 85 | (byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y', 86 | (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3', 87 | (byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8', 88 | (byte) '9', (byte) '-', (byte) '_'}; 89 | 90 | /** 91 | * Translates a Base64 value to either its 6-bit reconstruction value 92 | * or a negative number indicating some other meaning. 93 | **/ 94 | private final static byte[] DECODABET = {-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8 95 | -5, -5, // Whitespace: Tab and Linefeed 96 | -9, -9, // Decimal 11 - 12 97 | -5, // Whitespace: Carriage Return 98 | -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26 99 | -9, -9, -9, -9, -9, // Decimal 27 - 31 100 | -5, // Whitespace: Space 101 | -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42 102 | 62, // Plus sign at decimal 43 103 | -9, -9, -9, // Decimal 44 - 46 104 | 63, // Slash at decimal 47 105 | 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine 106 | -9, -9, -9, // Decimal 58 - 60 107 | -1, // Equals sign at decimal 61 108 | -9, -9, -9, // Decimal 62 - 64 109 | 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N' 110 | 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z' 111 | -9, -9, -9, -9, -9, -9, // Decimal 91 - 96 112 | 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm' 113 | 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z' 114 | -9, -9, -9, -9, -9 // Decimal 123 - 127 115 | /* ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139 116 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 117 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 118 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 119 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 120 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 121 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 122 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 123 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 124 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */ 125 | }; 126 | 127 | /** The web safe decodabet */ 128 | private final static byte[] WEBSAFE_DECODABET = 129 | {-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8 130 | -5, -5, // Whitespace: Tab and Linefeed 131 | -9, -9, // Decimal 11 - 12 132 | -5, // Whitespace: Carriage Return 133 | -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26 134 | -9, -9, -9, -9, -9, // Decimal 27 - 31 135 | -5, // Whitespace: Space 136 | -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 44 137 | 62, // Dash '-' sign at decimal 45 138 | -9, -9, // Decimal 46-47 139 | 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine 140 | -9, -9, -9, // Decimal 58 - 60 141 | -1, // Equals sign at decimal 61 142 | -9, -9, -9, // Decimal 62 - 64 143 | 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N' 144 | 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z' 145 | -9, -9, -9, -9, // Decimal 91-94 146 | 63, // Underscore '_' at decimal 95 147 | -9, // Decimal 96 148 | 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm' 149 | 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z' 150 | -9, -9, -9, -9, -9 // Decimal 123 - 127 151 | /* ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139 152 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 153 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 154 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 155 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 156 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 157 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 158 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 159 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 160 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */ 161 | }; 162 | 163 | // Indicates white space in encoding 164 | private final static byte WHITE_SPACE_ENC = -5; 165 | // Indicates equals sign in encoding 166 | private final static byte EQUALS_SIGN_ENC = -1; 167 | 168 | /** Defeats instantiation. */ 169 | private Base64() { 170 | } 171 | 172 | /* ******** E N C O D I N G M E T H O D S ******** */ 173 | 174 | /** 175 | * Encodes up to three bytes of the array source 176 | * and writes the resulting four Base64 bytes to destination. 177 | * The source and destination arrays can be manipulated 178 | * anywhere along their length by specifying 179 | * srcOffset and destOffset. 180 | * This method does not check to make sure your arrays 181 | * are large enough to accommodate srcOffset + 3 for 182 | * the source array or destOffset + 4 for 183 | * the destination array. 184 | * The actual number of significant bytes in your array is 185 | * given by numSigBytes. 186 | * 187 | * @param source the array to convert 188 | * @param srcOffset the index where conversion begins 189 | * @param numSigBytes the number of significant bytes in your array 190 | * @param destination the array to hold the conversion 191 | * @param destOffset the index where output will be put 192 | * @param alphabet is the encoding alphabet 193 | * @return the destination array 194 | * @since 1.3 195 | */ 196 | private static byte[] encode3to4(byte[] source, int srcOffset, 197 | int numSigBytes, byte[] destination, int destOffset, byte[] alphabet) { 198 | // 1 2 3 199 | // 01234567890123456789012345678901 Bit position 200 | // --------000000001111111122222222 Array position from threeBytes 201 | // --------| || || || | Six bit groups to index alphabet 202 | // >>18 >>12 >> 6 >> 0 Right shift necessary 203 | // 0x3f 0x3f 0x3f Additional AND 204 | 205 | // Create buffer with zero-padding if there are only one or two 206 | // significant bytes passed in the array. 207 | // We have to shift left 24 in order to flush out the 1's that appear 208 | // when Java treats a value as negative that is cast from a byte to an int. 209 | int inBuff = 210 | (numSigBytes > 0 ? ((source[srcOffset] << 24) >>> 8) : 0) 211 | | (numSigBytes > 1 ? ((source[srcOffset + 1] << 24) >>> 16) : 0) 212 | | (numSigBytes > 2 ? ((source[srcOffset + 2] << 24) >>> 24) : 0); 213 | 214 | switch (numSigBytes) { 215 | case 3: 216 | destination[destOffset] = alphabet[(inBuff >>> 18)]; 217 | destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f]; 218 | destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f]; 219 | destination[destOffset + 3] = alphabet[(inBuff) & 0x3f]; 220 | return destination; 221 | case 2: 222 | destination[destOffset] = alphabet[(inBuff >>> 18)]; 223 | destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f]; 224 | destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f]; 225 | destination[destOffset + 3] = EQUALS_SIGN; 226 | return destination; 227 | case 1: 228 | destination[destOffset] = alphabet[(inBuff >>> 18)]; 229 | destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f]; 230 | destination[destOffset + 2] = EQUALS_SIGN; 231 | destination[destOffset + 3] = EQUALS_SIGN; 232 | return destination; 233 | default: 234 | return destination; 235 | } // end switch 236 | } // end encode3to4 237 | 238 | /** 239 | * Encodes a byte array into Base64 notation. 240 | * Equivalent to calling 241 | * {@code encodeBytes(source, 0, source.length)} 242 | * 243 | * @param source The data to convert 244 | * @since 1.4 245 | */ 246 | public static String encode(byte[] source) { 247 | return encode(source, 0, source.length, ALPHABET, true); 248 | } 249 | 250 | /** 251 | * Encodes a byte array into web safe Base64 notation. 252 | * 253 | * @param source The data to convert 254 | * @param doPadding is {@code true} to pad result with '=' chars 255 | * if it does not fall on 3 byte boundaries 256 | */ 257 | public static String encodeWebSafe(byte[] source, boolean doPadding) { 258 | return encode(source, 0, source.length, WEBSAFE_ALPHABET, doPadding); 259 | } 260 | 261 | /** 262 | * Encodes a byte array into Base64 notation. 263 | * 264 | * @param source the data to convert 265 | * @param off offset in array where conversion should begin 266 | * @param len length of data to convert 267 | * @param alphabet the encoding alphabet 268 | * @param doPadding is {@code true} to pad result with '=' chars 269 | * if it does not fall on 3 byte boundaries 270 | * @since 1.4 271 | */ 272 | public static String encode(byte[] source, int off, int len, byte[] alphabet, 273 | boolean doPadding) { 274 | byte[] outBuff = encode(source, off, len, alphabet, Integer.MAX_VALUE); 275 | int outLen = outBuff.length; 276 | 277 | // If doPadding is false, set length to truncate '=' 278 | // padding characters 279 | while (doPadding == false && outLen > 0) { 280 | if (outBuff[outLen - 1] != '=') { 281 | break; 282 | } 283 | outLen -= 1; 284 | } 285 | 286 | return new String(outBuff, 0, outLen); 287 | } 288 | 289 | /** 290 | * Encodes a byte array into Base64 notation. 291 | * 292 | * @param source the data to convert 293 | * @param off offset in array where conversion should begin 294 | * @param len length of data to convert 295 | * @param alphabet is the encoding alphabet 296 | * @param maxLineLength maximum length of one line. 297 | * @return the BASE64-encoded byte array 298 | */ 299 | public static byte[] encode(byte[] source, int off, int len, byte[] alphabet, 300 | int maxLineLength) { 301 | int lenDiv3 = (len + 2) / 3; // ceil(len / 3) 302 | int len43 = lenDiv3 * 4; 303 | byte[] outBuff = new byte[len43 // Main 4:3 304 | + (len43 / maxLineLength)]; // New lines 305 | 306 | int d = 0; 307 | int e = 0; 308 | int len2 = len - 2; 309 | int lineLength = 0; 310 | for (; d < len2; d += 3, e += 4) { 311 | 312 | // The following block of code is the same as 313 | // encode3to4( source, d + off, 3, outBuff, e, alphabet ); 314 | // but inlined for faster encoding (~20% improvement) 315 | int inBuff = 316 | ((source[d + off] << 24) >>> 8) 317 | | ((source[d + 1 + off] << 24) >>> 16) 318 | | ((source[d + 2 + off] << 24) >>> 24); 319 | outBuff[e] = alphabet[(inBuff >>> 18)]; 320 | outBuff[e + 1] = alphabet[(inBuff >>> 12) & 0x3f]; 321 | outBuff[e + 2] = alphabet[(inBuff >>> 6) & 0x3f]; 322 | outBuff[e + 3] = alphabet[(inBuff) & 0x3f]; 323 | 324 | lineLength += 4; 325 | if (lineLength == maxLineLength) { 326 | outBuff[e + 4] = NEW_LINE; 327 | e++; 328 | lineLength = 0; 329 | } // end if: end of line 330 | } // end for: each piece of array 331 | 332 | if (d < len) { 333 | encode3to4(source, d + off, len - d, outBuff, e, alphabet); 334 | 335 | lineLength += 4; 336 | if (lineLength == maxLineLength) { 337 | // Add a last newline 338 | outBuff[e + 4] = NEW_LINE; 339 | e++; 340 | } 341 | e += 4; 342 | } 343 | 344 | assert (e == outBuff.length); 345 | return outBuff; 346 | } 347 | 348 | 349 | /* ******** D E C O D I N G M E T H O D S ******** */ 350 | 351 | 352 | /** 353 | * Decodes four bytes from array source 354 | * and writes the resulting bytes (up to three of them) 355 | * to destination. 356 | * The source and destination arrays can be manipulated 357 | * anywhere along their length by specifying 358 | * srcOffset and destOffset. 359 | * This method does not check to make sure your arrays 360 | * are large enough to accommodate srcOffset + 4 for 361 | * the source array or destOffset + 3 for 362 | * the destination array. 363 | * This method returns the actual number of bytes that 364 | * were converted from the Base64 encoding. 365 | * 366 | * 367 | * @param source the array to convert 368 | * @param srcOffset the index where conversion begins 369 | * @param destination the array to hold the conversion 370 | * @param destOffset the index where output will be put 371 | * @param decodabet the decodabet for decoding Base64 content 372 | * @return the number of decoded bytes converted 373 | * @since 1.3 374 | */ 375 | private static int decode4to3(byte[] source, int srcOffset, 376 | byte[] destination, int destOffset, byte[] decodabet) { 377 | // Example: Dk== 378 | if (source[srcOffset + 2] == EQUALS_SIGN) { 379 | int outBuff = 380 | ((decodabet[source[srcOffset]] << 24) >>> 6) 381 | | ((decodabet[source[srcOffset + 1]] << 24) >>> 12); 382 | 383 | destination[destOffset] = (byte) (outBuff >>> 16); 384 | return 1; 385 | } else if (source[srcOffset + 3] == EQUALS_SIGN) { 386 | // Example: DkL= 387 | int outBuff = 388 | ((decodabet[source[srcOffset]] << 24) >>> 6) 389 | | ((decodabet[source[srcOffset + 1]] << 24) >>> 12) 390 | | ((decodabet[source[srcOffset + 2]] << 24) >>> 18); 391 | 392 | destination[destOffset] = (byte) (outBuff >>> 16); 393 | destination[destOffset + 1] = (byte) (outBuff >>> 8); 394 | return 2; 395 | } else { 396 | // Example: DkLE 397 | int outBuff = 398 | ((decodabet[source[srcOffset]] << 24) >>> 6) 399 | | ((decodabet[source[srcOffset + 1]] << 24) >>> 12) 400 | | ((decodabet[source[srcOffset + 2]] << 24) >>> 18) 401 | | ((decodabet[source[srcOffset + 3]] << 24) >>> 24); 402 | 403 | destination[destOffset] = (byte) (outBuff >> 16); 404 | destination[destOffset + 1] = (byte) (outBuff >> 8); 405 | destination[destOffset + 2] = (byte) (outBuff); 406 | return 3; 407 | } 408 | } // end decodeToBytes 409 | 410 | 411 | /** 412 | * Decodes data from Base64 notation. 413 | * 414 | * @param s the string to decode (decoded in default encoding) 415 | * @return the decoded data 416 | * @since 1.4 417 | */ 418 | public static byte[] decode(String s) throws Base64DecoderException { 419 | byte[] bytes = s.getBytes(); 420 | return decode(bytes, 0, bytes.length); 421 | } 422 | 423 | /** 424 | * Decodes data from web safe Base64 notation. 425 | * Web safe encoding uses '-' instead of '+', '_' instead of '/' 426 | * 427 | * @param s the string to decode (decoded in default encoding) 428 | * @return the decoded data 429 | */ 430 | public static byte[] decodeWebSafe(String s) throws Base64DecoderException { 431 | byte[] bytes = s.getBytes(); 432 | return decodeWebSafe(bytes, 0, bytes.length); 433 | } 434 | 435 | /** 436 | * Decodes Base64 content in byte array format and returns 437 | * the decoded byte array. 438 | * 439 | * @param source The Base64 encoded data 440 | * @return decoded data 441 | * @since 1.3 442 | * @throws Base64DecoderException 443 | */ 444 | public static byte[] decode(byte[] source) throws Base64DecoderException { 445 | return decode(source, 0, source.length); 446 | } 447 | 448 | /** 449 | * Decodes web safe Base64 content in byte array format and returns 450 | * the decoded data. 451 | * Web safe encoding uses '-' instead of '+', '_' instead of '/' 452 | * 453 | * @param source the string to decode (decoded in default encoding) 454 | * @return the decoded data 455 | */ 456 | public static byte[] decodeWebSafe(byte[] source) 457 | throws Base64DecoderException { 458 | return decodeWebSafe(source, 0, source.length); 459 | } 460 | 461 | /** 462 | * Decodes Base64 content in byte array format and returns 463 | * the decoded byte array. 464 | * 465 | * @param source the Base64 encoded data 466 | * @param off the offset of where to begin decoding 467 | * @param len the length of characters to decode 468 | * @return decoded data 469 | * @since 1.3 470 | * @throws Base64DecoderException 471 | */ 472 | public static byte[] decode(byte[] source, int off, int len) 473 | throws Base64DecoderException { 474 | return decode(source, off, len, DECODABET); 475 | } 476 | 477 | /** 478 | * Decodes web safe Base64 content in byte array format and returns 479 | * the decoded byte array. 480 | * Web safe encoding uses '-' instead of '+', '_' instead of '/' 481 | * 482 | * @param source the Base64 encoded data 483 | * @param off the offset of where to begin decoding 484 | * @param len the length of characters to decode 485 | * @return decoded data 486 | */ 487 | public static byte[] decodeWebSafe(byte[] source, int off, int len) 488 | throws Base64DecoderException { 489 | return decode(source, off, len, WEBSAFE_DECODABET); 490 | } 491 | 492 | /** 493 | * Decodes Base64 content using the supplied decodabet and returns 494 | * the decoded byte array. 495 | * 496 | * @param source the Base64 encoded data 497 | * @param off the offset of where to begin decoding 498 | * @param len the length of characters to decode 499 | * @param decodabet the decodabet for decoding Base64 content 500 | * @return decoded data 501 | */ 502 | public static byte[] decode(byte[] source, int off, int len, byte[] decodabet) 503 | throws Base64DecoderException { 504 | int len34 = len * 3 / 4; 505 | byte[] outBuff = new byte[2 + len34]; // Upper limit on size of output 506 | int outBuffPosn = 0; 507 | 508 | byte[] b4 = new byte[4]; 509 | int b4Posn = 0; 510 | int i = 0; 511 | byte sbiCrop = 0; 512 | byte sbiDecode = 0; 513 | for (i = 0; i < len; i++) { 514 | sbiCrop = (byte) (source[i + off] & 0x7f); // Only the low seven bits 515 | sbiDecode = decodabet[sbiCrop]; 516 | 517 | if (sbiDecode >= WHITE_SPACE_ENC) { // White space Equals sign or better 518 | if (sbiDecode >= EQUALS_SIGN_ENC) { 519 | // An equals sign (for padding) must not occur at position 0 or 1 520 | // and must be the last byte[s] in the encoded value 521 | if (sbiCrop == EQUALS_SIGN) { 522 | int bytesLeft = len - i; 523 | byte lastByte = (byte) (source[len - 1 + off] & 0x7f); 524 | if (b4Posn == 0 || b4Posn == 1) { 525 | throw new Base64DecoderException( 526 | "invalid padding byte '=' at byte offset " + i); 527 | } else if ((b4Posn == 3 && bytesLeft > 2) 528 | || (b4Posn == 4 && bytesLeft > 1)) { 529 | throw new Base64DecoderException( 530 | "padding byte '=' falsely signals end of encoded value " 531 | + "at offset " + i); 532 | } else if (lastByte != EQUALS_SIGN && lastByte != NEW_LINE) { 533 | throw new Base64DecoderException( 534 | "encoded value has invalid trailing byte"); 535 | } 536 | break; 537 | } 538 | 539 | b4[b4Posn++] = sbiCrop; 540 | if (b4Posn == 4) { 541 | outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet); 542 | b4Posn = 0; 543 | } 544 | } 545 | } else { 546 | throw new Base64DecoderException("Bad Base64 input character at " + i 547 | + ": " + source[i + off] + "(decimal)"); 548 | } 549 | } 550 | 551 | // Because web safe encoding allows non padding base64 encodes, we 552 | // need to pad the rest of the b4 buffer with equal signs when 553 | // b4Posn != 0. There can be at most 2 equal signs at the end of 554 | // four characters, so the b4 buffer must have two or three 555 | // characters. This also catches the case where the input is 556 | // padded with EQUALS_SIGN 557 | if (b4Posn != 0) { 558 | if (b4Posn == 1) { 559 | throw new Base64DecoderException("single trailing character at offset " 560 | + (len - 1)); 561 | } 562 | b4[b4Posn++] = EQUALS_SIGN; 563 | outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet); 564 | } 565 | 566 | byte[] out = new byte[outBuffPosn]; 567 | System.arraycopy(outBuff, 0, out, 0, outBuffPosn); 568 | return out; 569 | } 570 | } 571 | -------------------------------------------------------------------------------- /AndroidBillingLibrary/src/net/robotmedia/billing/utils/Base64DecoderException.java: -------------------------------------------------------------------------------- 1 | // Copyright 2002, 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 | package net.robotmedia.billing.utils; 16 | 17 | /** 18 | * Exception thrown when encountering an invalid Base64 input character. 19 | * 20 | * @author nelson 21 | */ 22 | public class Base64DecoderException extends Exception { 23 | public Base64DecoderException() { 24 | super(); 25 | } 26 | 27 | public Base64DecoderException(String s) { 28 | super(s); 29 | } 30 | 31 | private static final long serialVersionUID = 1L; 32 | } 33 | -------------------------------------------------------------------------------- /AndroidBillingLibrary/src/net/robotmedia/billing/utils/Compatibility.java: -------------------------------------------------------------------------------- 1 | /* Copyright 2011 Robot Media SL (http://www.robotmedia.net) 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 net.robotmedia.billing.utils; 17 | 18 | import java.lang.reflect.Field; 19 | import java.lang.reflect.Method; 20 | 21 | import android.app.Activity; 22 | import android.app.Service; 23 | import android.content.Intent; 24 | import android.content.IntentSender; 25 | import android.util.Log; 26 | 27 | public class Compatibility { 28 | private static Method startIntentSender; 29 | public static int START_NOT_STICKY; 30 | @SuppressWarnings("rawtypes") 31 | private static final Class[] START_INTENT_SENDER_SIG = new Class[] { 32 | IntentSender.class, Intent.class, int.class, int.class, int.class 33 | }; 34 | 35 | static { 36 | initCompatibility(); 37 | }; 38 | 39 | private static void initCompatibility() { 40 | try { 41 | final Field field = Service.class.getField("START_NOT_STICKY"); 42 | START_NOT_STICKY = field.getInt(null); 43 | } catch (Exception e) { 44 | START_NOT_STICKY = 2; 45 | } 46 | try { 47 | startIntentSender = Activity.class.getMethod("startIntentSender", 48 | START_INTENT_SENDER_SIG); 49 | } catch (SecurityException e) { 50 | startIntentSender = null; 51 | } catch (NoSuchMethodException e) { 52 | startIntentSender = null; 53 | } 54 | } 55 | 56 | public static void startIntentSender(Activity activity, IntentSender intentSender, Intent intent) { 57 | if (startIntentSender != null) { 58 | final Object[] args = new Object[5]; 59 | args[0] = intentSender; 60 | args[1] = intent; 61 | args[2] = Integer.valueOf(0); 62 | args[3] = Integer.valueOf(0); 63 | args[4] = Integer.valueOf(0); 64 | try { 65 | startIntentSender.invoke(activity, args); 66 | } catch (Exception e) { 67 | Log.e(Compatibility.class.getSimpleName(), "startIntentSender", e); 68 | } 69 | } 70 | } 71 | 72 | public static boolean isStartIntentSenderSupported() { 73 | return startIntentSender != null; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /AndroidBillingLibrary/src/net/robotmedia/billing/utils/Installation.java: -------------------------------------------------------------------------------- 1 | /* Copyright 2011 Robot Media SL (http://www.robotmedia.net) 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 net.robotmedia.billing.utils; 17 | 18 | import java.io.File; 19 | import java.io.FileOutputStream; 20 | import java.io.IOException; 21 | import java.io.RandomAccessFile; 22 | import java.util.UUID; 23 | 24 | import android.content.Context; 25 | 26 | public class Installation { 27 | private static final String INSTALLATION = "INSTALLATION"; 28 | private static String sID = null; 29 | 30 | public synchronized static String id(Context context) { 31 | if (sID == null) { 32 | File installation = new File(context.getFilesDir(), INSTALLATION); 33 | try { 34 | if (!installation.exists()) { 35 | writeInstallationFile(installation); 36 | } 37 | sID = readInstallationFile(installation); 38 | } catch (Exception e) { 39 | throw new RuntimeException(e); 40 | } 41 | } 42 | return sID; 43 | } 44 | 45 | private static String readInstallationFile(File installation) throws IOException { 46 | RandomAccessFile f = new RandomAccessFile(installation, "r"); 47 | byte[] bytes = new byte[(int) f.length()]; 48 | f.readFully(bytes); 49 | f.close(); 50 | return new String(bytes); 51 | } 52 | 53 | private static void writeInstallationFile(File installation) throws IOException { 54 | FileOutputStream out = new FileOutputStream(installation); 55 | String id = UUID.randomUUID().toString(); 56 | out.write(id.getBytes()); 57 | out.close(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /AndroidBillingLibrary/src/net/robotmedia/billing/utils/Security.java: -------------------------------------------------------------------------------- 1 | /* Copyright 2011 Robot Media SL (http://www.robotmedia.net) 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 net.robotmedia.billing.utils; 17 | 18 | import java.security.SecureRandom; 19 | import java.util.HashSet; 20 | 21 | import net.robotmedia.billing.utils.AESObfuscator.ValidationException; 22 | 23 | import android.content.Context; 24 | import android.provider.Settings; 25 | import android.util.Log; 26 | 27 | public class Security { 28 | 29 | private static HashSet knownNonces = new HashSet(); 30 | private static final SecureRandom RANDOM = new SecureRandom(); 31 | private static final String TAG = Security.class.getSimpleName(); 32 | 33 | /** Generates a nonce (a random number used once). */ 34 | public static long generateNonce() { 35 | long nonce = RANDOM.nextLong(); 36 | knownNonces.add(nonce); 37 | return nonce; 38 | } 39 | 40 | public static boolean isNonceKnown(long nonce) { 41 | return knownNonces.contains(nonce); 42 | } 43 | 44 | public static void removeNonce(long nonce) { 45 | knownNonces.remove(nonce); 46 | } 47 | 48 | public static String obfuscate(Context context, byte[] salt, String original) { 49 | final AESObfuscator obfuscator = getObfuscator(context, salt); 50 | return obfuscator.obfuscate(original); 51 | } 52 | 53 | private static AESObfuscator _obfuscator = null; 54 | 55 | private static AESObfuscator getObfuscator(Context context, byte[] salt) { 56 | if (_obfuscator == null) { 57 | final String installationId = Installation.id(context); 58 | final String deviceId = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID); 59 | final String password = installationId + deviceId + context.getPackageName(); 60 | _obfuscator = new AESObfuscator(salt, password); 61 | } 62 | return _obfuscator; 63 | } 64 | 65 | public static String unobfuscate(Context context, byte[] salt, String obfuscated) { 66 | final AESObfuscator obfuscator = getObfuscator(context, salt); 67 | try { 68 | return obfuscator.unobfuscate(obfuscated); 69 | } catch (ValidationException e) { 70 | Log.w(TAG, "Invalid obfuscated data or key"); 71 | } 72 | return null; 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /AndroidBillingLibraryTest/.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /AndroidBillingLibraryTest/.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | gen -------------------------------------------------------------------------------- /AndroidBillingLibraryTest/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | AndroidBillingLibraryTest 4 | 5 | 6 | 7 | 8 | 9 | com.android.ide.eclipse.adt.ResourceManagerBuilder 10 | 11 | 12 | 13 | 14 | com.android.ide.eclipse.adt.PreCompilerBuilder 15 | 16 | 17 | 18 | 19 | org.eclipse.jdt.core.javabuilder 20 | 21 | 22 | 23 | 24 | com.android.ide.eclipse.adt.ApkBuilder 25 | 26 | 27 | 28 | 29 | 30 | com.android.ide.eclipse.adt.AndroidNature 31 | org.eclipse.jdt.core.javanature 32 | 33 | 34 | -------------------------------------------------------------------------------- /AndroidBillingLibraryTest/.settings/org.eclipse.jdt.core.prefs: -------------------------------------------------------------------------------- 1 | eclipse.preferences.version=1 2 | org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.5 3 | org.eclipse.jdt.core.compiler.compliance=1.5 4 | org.eclipse.jdt.core.compiler.source=1.5 5 | -------------------------------------------------------------------------------- /AndroidBillingLibraryTest/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /AndroidBillingLibraryTest/proguard.cfg: -------------------------------------------------------------------------------- 1 | -optimizationpasses 5 2 | -dontusemixedcaseclassnames 3 | -dontskipnonpubliclibraryclasses 4 | -dontpreverify 5 | -verbose 6 | -optimizations !code/simplification/arithmetic,!field/*,!class/merging/* 7 | 8 | -keep public class * extends android.app.Activity 9 | -keep public class * extends android.app.Application 10 | -keep public class * extends android.app.Service 11 | -keep public class * extends android.content.BroadcastReceiver 12 | -keep public class * extends android.content.ContentProvider 13 | -keep public class * extends android.app.backup.BackupAgentHelper 14 | -keep public class * extends android.preference.Preference 15 | -keep public class com.android.vending.licensing.ILicensingService 16 | 17 | -keepclasseswithmembers class * { 18 | native ; 19 | } 20 | 21 | -keepclasseswithmembers class * { 22 | public (android.content.Context, android.util.AttributeSet); 23 | } 24 | 25 | -keepclasseswithmembers class * { 26 | public (android.content.Context, android.util.AttributeSet, int); 27 | } 28 | 29 | -keepclassmembers enum * { 30 | public static **[] values(); 31 | public static ** valueOf(java.lang.String); 32 | } 33 | 34 | -keep class * implements android.os.Parcelable { 35 | public static final android.os.Parcelable$Creator *; 36 | } 37 | -------------------------------------------------------------------------------- /AndroidBillingLibraryTest/project.properties: -------------------------------------------------------------------------------- 1 | # This file is automatically generated by Android Tools. 2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED! 3 | # 4 | # This file must be checked in Version Control Systems. 5 | # 6 | # To customize properties used by the Ant build system edit 7 | # "ant.properties", and override values to adapt the script to your 8 | # project structure. 9 | # 10 | # To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): 11 | #proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt 12 | 13 | android.library.reference.1=../AndroidBillingLibrary 14 | # Project target. 15 | target=android-13 16 | -------------------------------------------------------------------------------- /AndroidBillingLibraryTest/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | AndroidBillingLibraryTest 4 | 5 | -------------------------------------------------------------------------------- /AndroidBillingLibraryTest/src/net/robotmedia/billing/BillingControllerTest.java: -------------------------------------------------------------------------------- 1 | /* Copyright 2011 Robot Media SL (http://www.robotmedia.net) 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 net.robotmedia.billing; 17 | 18 | import java.util.HashSet; 19 | import java.util.List; 20 | import java.util.Set; 21 | 22 | import net.robotmedia.billing.BillingController.BillingStatus; 23 | import net.robotmedia.billing.BillingRequest.ResponseCode; 24 | import net.robotmedia.billing.helper.MockBillingObserver; 25 | import net.robotmedia.billing.model.BillingDB; 26 | import net.robotmedia.billing.model.BillingDBTest; 27 | import net.robotmedia.billing.model.Transaction; 28 | import net.robotmedia.billing.model.TransactionTest; 29 | import android.test.AndroidTestCase; 30 | import android.test.suitebuilder.annotation.MediumTest; 31 | import android.test.suitebuilder.annotation.SmallTest; 32 | 33 | public class BillingControllerTest extends AndroidTestCase { 34 | 35 | private BillingDB mData; 36 | 37 | @Override 38 | protected void tearDown() throws Exception { 39 | super.tearDown(); 40 | BillingDBTest.deleteDB(mData); 41 | } 42 | 43 | @Override 44 | protected void setUp() throws Exception { 45 | super.setUp(); 46 | mData = new BillingDB(getContext()); 47 | } 48 | 49 | @SmallTest 50 | public void testCheckBillingSupported() throws Exception { 51 | BillingController.checkBillingSupported(getContext()); 52 | } 53 | 54 | @SmallTest 55 | public void testCheckSubscriptionSupported() throws Exception { 56 | BillingController.checkSubscriptionSupported(getContext()); 57 | } 58 | 59 | @MediumTest 60 | public void testIsPurchased() throws Exception { 61 | assertFalse(BillingController.isPurchased(getContext(), TransactionTest.TRANSACTION_1.productId)); 62 | BillingController.storeTransaction(getContext(), TransactionTest.TRANSACTION_1); 63 | assertTrue(BillingController.isPurchased(getContext(), TransactionTest.TRANSACTION_1.productId)); 64 | BillingController.storeTransaction(getContext(), TransactionTest.TRANSACTION_1_REFUNDED); 65 | assertTrue(BillingController.isPurchased(getContext(), TransactionTest.TRANSACTION_1.productId)); 66 | } 67 | 68 | @MediumTest 69 | public void testCountPurchases() throws Exception { 70 | assertEquals(BillingController.countPurchases(getContext(), TransactionTest.TRANSACTION_1.productId), 0); 71 | BillingController.storeTransaction(getContext(), TransactionTest.TRANSACTION_1); 72 | assertEquals(BillingController.countPurchases(getContext(), TransactionTest.TRANSACTION_1.productId), 1); 73 | BillingController.storeTransaction(getContext(), TransactionTest.TRANSACTION_1_REFUNDED); 74 | assertEquals(BillingController.countPurchases(getContext(), TransactionTest.TRANSACTION_1.productId), 1); 75 | BillingController.storeTransaction(getContext(), TransactionTest.TRANSACTION_2); 76 | assertEquals(BillingController.countPurchases(getContext(), TransactionTest.TRANSACTION_1.productId), 1); 77 | } 78 | 79 | @MediumTest 80 | public void testGetTransactions() throws Exception { 81 | final List transactions0 = BillingController.getTransactions(getContext()); 82 | assertEquals(transactions0.size(), 0); 83 | BillingController.storeTransaction(getContext(), TransactionTest.TRANSACTION_1); 84 | final List transactions1 = BillingController.getTransactions(getContext()); 85 | assertEquals(transactions1.size(), 1); 86 | BillingController.storeTransaction(getContext(), TransactionTest.TRANSACTION_2_REFUNDED); 87 | final List transactions2 = BillingController.getTransactions(getContext()); 88 | assertEquals(transactions2.size(), 2); 89 | } 90 | 91 | @MediumTest 92 | public void testGetTransactionsString() throws Exception { 93 | final List transactions0 = BillingController.getTransactions(getContext()); 94 | assertEquals(transactions0.size(), 0); 95 | BillingController.storeTransaction(getContext(), TransactionTest.TRANSACTION_1); 96 | final List transactions1 = BillingController.getTransactions(getContext(), TransactionTest.TRANSACTION_1.productId); 97 | assertEquals(transactions1.size(), 1); 98 | BillingController.storeTransaction(getContext(), TransactionTest.TRANSACTION_2_REFUNDED); 99 | final List transactions2 = BillingController.getTransactions(getContext(), TransactionTest.TRANSACTION_1.productId); 100 | assertEquals(transactions2.size(), 1); 101 | } 102 | 103 | @SmallTest 104 | public void testOnTransactionRestored() throws Exception { 105 | final Set flags = new HashSet(); 106 | final IBillingObserver observer = new MockBillingObserver() { 107 | @Override 108 | public void onTransactionsRestored() { 109 | flags.add(true); 110 | } 111 | }; 112 | BillingController.registerObserver(observer); 113 | BillingController.onTransactionsRestored(); 114 | assertEquals(flags.size(), 1); 115 | BillingController.unregisterObserver(observer); 116 | } 117 | 118 | @SmallTest 119 | public void testOnRequestPurchaseResponse() throws Exception { 120 | final String testItemId = TransactionTest.TRANSACTION_1.productId; 121 | final ResponseCode testResponse = ResponseCode.RESULT_OK; 122 | final Set flags = new HashSet(); 123 | final IBillingObserver observer = new MockBillingObserver() { 124 | @Override 125 | public void onRequestPurchaseResponse(String itemId, ResponseCode response) { 126 | flags.add(true); 127 | assertEquals(testItemId, itemId); 128 | assertEquals(testResponse, response); 129 | } 130 | }; 131 | BillingController.registerObserver(observer); 132 | BillingController.onRequestPurchaseResponse(testItemId, testResponse); 133 | assertEquals(flags.size(), 1); 134 | BillingController.unregisterObserver(observer); 135 | } 136 | 137 | public void testOnBillingCheckedSupportedTrue() throws Exception { 138 | final Set flags = new HashSet(); 139 | final IBillingObserver observer = new MockBillingObserver() { 140 | @Override 141 | public void onBillingChecked(boolean supported) { 142 | flags.add(true); 143 | assertTrue(supported); 144 | } 145 | }; 146 | BillingController.registerObserver(observer); 147 | BillingController.onBillingChecked(true); 148 | assertEquals(flags.size(), 1); 149 | assertEquals(BillingController.checkBillingSupported(getContext()), BillingStatus.SUPPORTED); 150 | BillingController.unregisterObserver(observer); 151 | } 152 | 153 | public void testOnBillingCheckedSupportedFalse() throws Exception { 154 | final Set flags = new HashSet(); 155 | final IBillingObserver observer = new MockBillingObserver() { 156 | @Override 157 | public void onBillingChecked(boolean supported) { 158 | flags.add(true); 159 | assertFalse(supported); 160 | } 161 | }; 162 | BillingController.registerObserver(observer); 163 | BillingController.onBillingChecked(false); 164 | assertEquals(flags.size(), 1); 165 | assertEquals(BillingController.checkBillingSupported(getContext()), BillingStatus.UNSUPPORTED); 166 | assertEquals(BillingController.checkSubscriptionSupported(getContext()), BillingStatus.UNSUPPORTED); 167 | BillingController.unregisterObserver(observer); 168 | } 169 | 170 | public void testOnSubscriptionCheckedSupportedTrue() throws Exception { 171 | final Set flags = new HashSet(); 172 | final IBillingObserver observer = new MockBillingObserver() { 173 | @Override 174 | public void onSubscriptionChecked(boolean supported) { 175 | flags.add(true); 176 | assertTrue(supported); 177 | } 178 | }; 179 | BillingController.registerObserver(observer); 180 | BillingController.onSubscriptionChecked(true); 181 | assertEquals(flags.size(), 1); 182 | assertEquals(BillingController.checkBillingSupported(getContext()), BillingStatus.SUPPORTED); 183 | assertEquals(BillingController.checkSubscriptionSupported(getContext()), BillingStatus.SUPPORTED); 184 | BillingController.unregisterObserver(observer); 185 | } 186 | 187 | public void testOnSubscriptionCheckedSupportedFalse() throws Exception { 188 | final Set flags = new HashSet(); 189 | final IBillingObserver observer = new MockBillingObserver() { 190 | @Override 191 | public void onSubscriptionChecked(boolean supported) { 192 | flags.add(true); 193 | assertFalse(supported); 194 | } 195 | }; 196 | BillingController.registerObserver(observer); 197 | BillingController.onSubscriptionChecked(false); 198 | assertEquals(flags.size(), 1); 199 | assertEquals(BillingController.checkSubscriptionSupported(getContext()), BillingStatus.UNSUPPORTED); 200 | BillingController.unregisterObserver(observer); 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /AndroidBillingLibraryTest/src/net/robotmedia/billing/BillingReceiverTest.java: -------------------------------------------------------------------------------- 1 | /* Copyright 2011 Robot Media SL (http://www.robotmedia.net) 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 net.robotmedia.billing; 17 | 18 | import net.robotmedia.billing.BillingRequest.ResponseCode; 19 | import android.content.Intent; 20 | import android.test.AndroidTestCase; 21 | import android.test.suitebuilder.annotation.SmallTest; 22 | 23 | public class BillingReceiverTest extends AndroidTestCase { 24 | 25 | private BillingReceiver mReceiver; 26 | 27 | @Override 28 | protected void setUp() throws Exception { 29 | super.setUp(); 30 | mReceiver = new BillingReceiver(); 31 | } 32 | 33 | @SmallTest 34 | public void testNotify() throws Exception { 35 | final Intent intent = new Intent(BillingReceiver.ACTION_NOTIFY); 36 | intent.putExtra(BillingReceiver.EXTRA_NOTIFICATION_ID, "notificationId"); 37 | mReceiver.onReceive(getContext(), intent); 38 | } 39 | 40 | @SmallTest 41 | public void testResponseCode() throws Exception { 42 | final Intent intent = new Intent(BillingReceiver.ACTION_RESPONSE_CODE); 43 | intent.putExtra(BillingReceiver.EXTRA_REQUEST_ID, "requestId"); 44 | intent.putExtra(BillingReceiver.EXTRA_RESPONSE_CODE, ResponseCode.RESULT_OK.ordinal()); 45 | mReceiver.onReceive(getContext(), intent); 46 | } 47 | 48 | @SmallTest 49 | public void testPurchaseStateChanged() throws Exception { 50 | final Intent intent = new Intent(BillingReceiver.ACTION_PURCHASE_STATE_CHANGED); 51 | intent.putExtra(BillingReceiver.EXTRA_INAPP_SIGNED_DATA, ""); 52 | intent.putExtra(BillingReceiver.EXTRA_INAPP_SIGNATURE, ""); 53 | mReceiver.onReceive(getContext(), intent); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /AndroidBillingLibraryTest/src/net/robotmedia/billing/BillingServiceTest.java: -------------------------------------------------------------------------------- 1 | /* Copyright 2011 Robot Media SL (http://www.robotmedia.net) 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 net.robotmedia.billing; 17 | 18 | import android.content.Intent; 19 | import android.os.IBinder; 20 | import android.test.ServiceTestCase; 21 | import android.test.suitebuilder.annotation.SmallTest; 22 | 23 | public class BillingServiceTest extends ServiceTestCase { 24 | 25 | private final static long NONCE = 147; 26 | private final static String[] NOTIFY_IDS = new String[] { "test" }; 27 | private final static String ITEM_ID = "android.test.purchased"; 28 | 29 | public BillingServiceTest() { 30 | super(BillingService.class); 31 | } 32 | 33 | @SmallTest 34 | public void testStart() throws Exception { 35 | final Intent intent = new Intent(); 36 | intent.setClass(getContext(), BillingService.class); 37 | startService(intent); 38 | } 39 | 40 | @SmallTest 41 | public void testCheckBillingSupported() throws Exception { 42 | BillingService.checkBillingSupported(getContext()); 43 | } 44 | 45 | @SmallTest 46 | public void testCheckSubscriptionSupported() throws Exception { 47 | BillingService.checkSubscriptionSupported(getContext()); 48 | } 49 | 50 | @SmallTest 51 | public void testConfirmNotifications() throws Exception { 52 | BillingService.confirmNotifications(getContext(), NOTIFY_IDS); 53 | } 54 | 55 | @SmallTest 56 | public void testGetPurchaseInformation() throws Exception { 57 | BillingService.getPurchaseInformation(getContext(), NOTIFY_IDS, NONCE); 58 | } 59 | 60 | @SmallTest 61 | public void testRequestPurchase() throws Exception { 62 | BillingService.requestPurchase(getContext(), ITEM_ID, null); 63 | } 64 | 65 | @SmallTest 66 | public void testRequestSubscription() throws Exception { 67 | BillingService.requestSubscription(getContext(), ITEM_ID, null); 68 | } 69 | 70 | @SmallTest 71 | public void testRestoreTransactions() throws Exception { 72 | BillingService.restoreTransations(getContext(), NONCE); 73 | } 74 | 75 | @SmallTest 76 | public void testBind() { 77 | final Intent intent = new Intent(); 78 | intent.setClass(getContext(), BillingService.class); 79 | final IBinder service = bindService(intent); 80 | assertNull(service); 81 | } 82 | } -------------------------------------------------------------------------------- /AndroidBillingLibraryTest/src/net/robotmedia/billing/helper/AbstractBillingActivityTest.java: -------------------------------------------------------------------------------- 1 | /* Copyright 2011 Robot Media SL (http://www.robotmedia.net) 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 net.robotmedia.billing.helper; 17 | 18 | import android.test.ActivityInstrumentationTestCase2; 19 | import android.test.suitebuilder.annotation.MediumTest; 20 | 21 | public class AbstractBillingActivityTest extends ActivityInstrumentationTestCase2 { 22 | 23 | // private static final String PURCHASE_ID = "android.example.purchased"; 24 | 25 | private MockBillingActivity mActivity; 26 | 27 | public AbstractBillingActivityTest() { 28 | super(MockBillingActivity.class); 29 | } 30 | 31 | @Override 32 | protected void setUp() throws Exception { 33 | super.setUp(); 34 | mActivity = this.getActivity(); 35 | } 36 | 37 | @MediumTest 38 | public void testCheckBillingSupported() throws Exception { 39 | mActivity.checkBillingSupported(); 40 | } 41 | 42 | @MediumTest 43 | public void testCheckSubscriptionSupported() throws Exception { 44 | mActivity.checkSubscriptionSupported(); 45 | } 46 | 47 | // TODO: Find a way to test the following without hanging the test suite 48 | // @SmallTest 49 | // public void testOnPurchaseIntent() throws Exception { 50 | // final PendingIntent purchaseIntent = PendingIntent.getActivity(mActivity, 51 | // 0, new Intent(), 0); 52 | // mActivity.onPurchaseIntent(PURCHASE_ID, purchaseIntent); 53 | // } 54 | // @MediumTest 55 | // public void testRequestPurchase() throws Exception { 56 | // mActivity.requestPurchase(PURCHASE_ID); 57 | // } 58 | 59 | @MediumTest 60 | public void testRestoreTransactions() throws Exception { 61 | mActivity.restoreTransactions(); 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /AndroidBillingLibraryTest/src/net/robotmedia/billing/helper/MockBillingActivity.java: -------------------------------------------------------------------------------- 1 | /* Copyright 2011 Robot Media SL (http://www.robotmedia.net) 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 net.robotmedia.billing.helper; 17 | 18 | import net.robotmedia.billing.BillingRequest.ResponseCode; 19 | import net.robotmedia.billing.model.Transaction.PurchaseState; 20 | 21 | public class MockBillingActivity extends AbstractBillingActivity { 22 | 23 | @Override 24 | public void onBillingChecked(boolean supported) { 25 | // TODO Auto-generated method stub 26 | } 27 | 28 | @Override 29 | public void onSubscriptionChecked(boolean supported) { 30 | // TODO Auto-generated method stub 31 | } 32 | 33 | public byte[] getObfuscationSalt() { 34 | // TODO Auto-generated method stub 35 | return null; 36 | } 37 | 38 | public String getPublicKey() { 39 | // TODO Auto-generated method stub 40 | return null; 41 | } 42 | 43 | @Override 44 | public void onRequestPurchaseResponse(String itemId, ResponseCode response) { 45 | // TODO Auto-generated method stub 46 | } 47 | 48 | @Override 49 | public void onPurchaseStateChanged(String itemId, PurchaseState state) { 50 | // TODO Auto-generated method stub 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /AndroidBillingLibraryTest/src/net/robotmedia/billing/helper/MockBillingObserver.java: -------------------------------------------------------------------------------- 1 | package net.robotmedia.billing.helper; 2 | 3 | import android.app.PendingIntent; 4 | import net.robotmedia.billing.BillingRequest.ResponseCode; 5 | import net.robotmedia.billing.IBillingObserver; 6 | import net.robotmedia.billing.model.Transaction.PurchaseState; 7 | 8 | public class MockBillingObserver implements IBillingObserver { 9 | 10 | public void onBillingChecked(boolean supported) { 11 | } 12 | 13 | public void onSubscriptionChecked(boolean supported) { 14 | } 15 | 16 | public void onPurchaseIntent(String itemId, PendingIntent purchaseIntent) { 17 | } 18 | 19 | public void onPurchaseStateChanged(String itemId, PurchaseState state) { 20 | } 21 | 22 | public void onRequestPurchaseResponse(String itemId, ResponseCode response) { 23 | } 24 | 25 | public void onTransactionsRestored() { 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /AndroidBillingLibraryTest/src/net/robotmedia/billing/model/BillingDBTest.java: -------------------------------------------------------------------------------- 1 | /* Copyright 2011 Robot Media SL (http://www.robotmedia.net) 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 net.robotmedia.billing.model; 17 | 18 | import android.database.Cursor; 19 | import android.test.AndroidTestCase; 20 | import android.test.suitebuilder.annotation.SmallTest; 21 | 22 | public class BillingDBTest extends AndroidTestCase { 23 | 24 | private BillingDB mData; 25 | 26 | public static void assertEqualsFromDb(Transaction a, Transaction b) { 27 | assertEquals(a.orderId, b.orderId); 28 | assertEquals(a.productId, b.productId); 29 | assertEquals(a.purchaseState, b.purchaseState); 30 | assertEquals(a.purchaseTime, b.purchaseTime); 31 | assertEquals(a.developerPayload, b.developerPayload); 32 | } 33 | 34 | @Override 35 | protected void setUp() throws Exception { 36 | super.setUp(); 37 | mData = new BillingDB(getContext()); 38 | } 39 | 40 | public static final void deleteDB(BillingDB data) { 41 | data.mDb.delete(BillingDB.TABLE_TRANSACTIONS, null, null); 42 | data.close(); 43 | } 44 | 45 | @Override 46 | protected void tearDown() throws Exception { 47 | super.tearDown(); 48 | deleteDB(mData); 49 | } 50 | 51 | @SmallTest 52 | public void testInsert() throws Exception { 53 | mData.insert(TransactionTest.TRANSACTION_1); 54 | final Cursor cursor = mData.queryTransactions(); 55 | assertEquals(cursor.getCount(), 1); 56 | cursor.moveToNext(); 57 | final Transaction stored = BillingDB.createTransaction(cursor); 58 | stored.packageName = TransactionTest.TRANSACTION_1.packageName; // Not stored in DB 59 | stored.notificationId = TransactionTest.TRANSACTION_1.notificationId; // Not stored in DB 60 | assertEqualsFromDb(TransactionTest.TRANSACTION_1, stored); 61 | } 62 | 63 | @SmallTest 64 | public void testUnique() throws Exception { 65 | mData.insert(TransactionTest.TRANSACTION_1); 66 | mData.insert(TransactionTest.TRANSACTION_1); 67 | final Cursor cursor = mData.queryTransactions(); 68 | assertEquals(cursor.getCount(), 1); 69 | } 70 | 71 | @SmallTest 72 | public void testQueryTransactions() throws Exception { 73 | final Cursor cursor1 = mData.queryTransactions(); 74 | assertEquals(cursor1.getCount(), 0); 75 | cursor1.close(); 76 | 77 | mData.insert(TransactionTest.TRANSACTION_1); 78 | final Cursor cursor2 = mData.queryTransactions(); 79 | assertEquals(cursor2.getCount(), 1); 80 | cursor2.moveToNext(); 81 | final Transaction stored = BillingDB.createTransaction(cursor2); 82 | stored.packageName = TransactionTest.TRANSACTION_1.packageName; // Not stored in DB 83 | stored.notificationId = TransactionTest.TRANSACTION_1.notificationId; // Not stored in DB 84 | TransactionTest.assertEquals(TransactionTest.TRANSACTION_1, stored); 85 | cursor2.close(); 86 | 87 | mData.insert(TransactionTest.TRANSACTION_2_REFUNDED); 88 | final Cursor cursor3 = mData.queryTransactions(); 89 | assertEquals(cursor3.getCount(), 2); 90 | cursor3.close(); 91 | } 92 | 93 | @SmallTest 94 | public void testQueryTransactionsString() throws Exception { 95 | final Cursor cursor1 = mData.queryTransactions(TransactionTest.TRANSACTION_1.productId); 96 | assertEquals(cursor1.getCount(), 0); 97 | cursor1.close(); 98 | 99 | mData.insert(TransactionTest.TRANSACTION_1); 100 | final Cursor cursor2 = mData.queryTransactions(TransactionTest.TRANSACTION_1.productId); 101 | assertEquals(cursor2.getCount(), 1); 102 | cursor2.moveToNext(); 103 | final Transaction stored = BillingDB.createTransaction(cursor2); 104 | stored.packageName = TransactionTest.TRANSACTION_1.packageName; // Not stored in DB 105 | stored.notificationId = TransactionTest.TRANSACTION_1.notificationId; // Not stored in DB 106 | TransactionTest.assertEquals(TransactionTest.TRANSACTION_1, stored); 107 | cursor2.close(); 108 | 109 | mData.insert(TransactionTest.TRANSACTION_2_REFUNDED); 110 | final Cursor cursor3 = mData.queryTransactions(TransactionTest.TRANSACTION_1.productId); 111 | assertEquals(cursor3.getCount(), 1); 112 | cursor3.close(); 113 | } 114 | 115 | @SmallTest 116 | public void testQueryTransactionsStringPurchaseState() throws Exception { 117 | final Cursor cursor1 = mData.queryTransactions(TransactionTest.TRANSACTION_1.productId, TransactionTest.TRANSACTION_1.purchaseState); 118 | assertEquals(cursor1.getCount(), 0); 119 | cursor1.close(); 120 | 121 | mData.insert(TransactionTest.TRANSACTION_1); 122 | mData.insert(TransactionTest.TRANSACTION_2_REFUNDED); 123 | final Cursor cursor2 = mData.queryTransactions(TransactionTest.TRANSACTION_1.productId, TransactionTest.TRANSACTION_1.purchaseState); 124 | assertEquals(cursor2.getCount(), 1); 125 | cursor2.moveToNext(); 126 | final Transaction stored = BillingDB.createTransaction(cursor2); 127 | stored.packageName = TransactionTest.TRANSACTION_1.packageName; // Not stored in DB 128 | stored.notificationId = TransactionTest.TRANSACTION_1.notificationId; // Not stored in DB 129 | TransactionTest.assertEquals(TransactionTest.TRANSACTION_1, stored); 130 | cursor2.close(); 131 | } 132 | 133 | } 134 | -------------------------------------------------------------------------------- /AndroidBillingLibraryTest/src/net/robotmedia/billing/model/TransactionManagerTest.java: -------------------------------------------------------------------------------- 1 | /* Copyright 2011 Robot Media SL (http://www.robotmedia.net) 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 net.robotmedia.billing.model; 17 | 18 | import java.util.List; 19 | 20 | import android.test.AndroidTestCase; 21 | import android.test.suitebuilder.annotation.MediumTest; 22 | 23 | public class TransactionManagerTest extends AndroidTestCase { 24 | 25 | @Override 26 | protected void tearDown() throws Exception { 27 | super.tearDown(); 28 | BillingDB mData = new BillingDB(getContext()); 29 | mData.mDb.delete(BillingDB.TABLE_TRANSACTIONS, null, null); 30 | mData.close(); 31 | } 32 | 33 | @MediumTest 34 | public void testAddPurchase() throws Exception { 35 | TransactionManager.addTransaction(getContext(), TransactionTest.TRANSACTION_1); 36 | final List purchases = TransactionManager.getTransactions(getContext()); 37 | assertEquals(purchases.size(), 1); 38 | final Transaction stored = purchases.get(0); 39 | BillingDBTest.assertEqualsFromDb(TransactionTest.TRANSACTION_1, stored); 40 | } 41 | 42 | @MediumTest 43 | public void testCountPurchases() throws Exception { 44 | assertEquals(TransactionManager.countPurchases(getContext(), TransactionTest.TRANSACTION_1.productId), 0); 45 | TransactionManager.addTransaction(getContext(), TransactionTest.TRANSACTION_1); 46 | assertEquals(TransactionManager.countPurchases(getContext(), TransactionTest.TRANSACTION_1.productId), 1); 47 | final Transaction newOrder = TransactionTest.TRANSACTION_1.clone(); 48 | newOrder.orderId = "newOrder"; 49 | TransactionManager.addTransaction(getContext(), newOrder); 50 | assertEquals(TransactionManager.countPurchases(getContext(), TransactionTest.TRANSACTION_1.productId), 2); 51 | } 52 | 53 | @MediumTest 54 | public void testIsPurchased() throws Exception { 55 | assertFalse(TransactionManager.isPurchased(getContext(), TransactionTest.TRANSACTION_1.productId)); 56 | TransactionManager.addTransaction(getContext(), TransactionTest.TRANSACTION_1); 57 | assertTrue(TransactionManager.isPurchased(getContext(), TransactionTest.TRANSACTION_1.productId)); 58 | } 59 | 60 | @MediumTest 61 | public void testGetTransactions() throws Exception { 62 | final List transactions1 = TransactionManager.getTransactions(getContext()); 63 | assertEquals(transactions1.size(), 0); 64 | TransactionManager.addTransaction(getContext(), TransactionTest.TRANSACTION_1); 65 | final List transactions2 = TransactionManager.getTransactions(getContext()); 66 | assertEquals(transactions2.size(), 1); 67 | TransactionManager.addTransaction(getContext(), TransactionTest.TRANSACTION_2_REFUNDED); 68 | final List transactions3 = TransactionManager.getTransactions(getContext()); 69 | assertEquals(transactions3.size(), 2); 70 | } 71 | 72 | @MediumTest 73 | public void testGetTransactionsString() throws Exception { 74 | final List transactions1 = TransactionManager.getTransactions(getContext(), TransactionTest.TRANSACTION_1.productId); 75 | assertEquals(transactions1.size(), 0); 76 | TransactionManager.addTransaction(getContext(), TransactionTest.TRANSACTION_1); 77 | final List transactions2 = TransactionManager.getTransactions(getContext(), TransactionTest.TRANSACTION_1.productId); 78 | assertEquals(transactions2.size(), 1); 79 | TransactionManager.addTransaction(getContext(), TransactionTest.TRANSACTION_2_REFUNDED); 80 | final List transactions3 = TransactionManager.getTransactions(getContext(), TransactionTest.TRANSACTION_1.productId); 81 | assertEquals(transactions3.size(), 1); 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /AndroidBillingLibraryTest/src/net/robotmedia/billing/model/TransactionTest.java: -------------------------------------------------------------------------------- 1 | /* Copyright 2011 Robot Media SL (http://www.robotmedia.net) 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 net.robotmedia.billing.model; 17 | 18 | import java.util.Date; 19 | 20 | import org.json.JSONObject; 21 | 22 | import android.test.suitebuilder.annotation.SmallTest; 23 | 24 | import junit.framework.TestCase; 25 | 26 | public class TransactionTest extends TestCase { 27 | 28 | public static final Transaction TRANSACTION_1 = new Transaction("order1", "android.test.purchased", "com.example", Transaction.PurchaseState.PURCHASED, "notificationId", new Date().getTime(), "developerPayload"); 29 | public static final Transaction TRANSACTION_2 = new Transaction("order2", "product_2", "com.example", Transaction.PurchaseState.PURCHASED, "notificationId", new Date().getTime(), "developerPayload"); 30 | public static final Transaction TRANSACTION_2_REFUNDED = new Transaction("order4", "product_2", "com.example", Transaction.PurchaseState.REFUNDED, "notificationId", new Date().getTime(), "developerPayload"); 31 | public static final Transaction TRANSACTION_1_REFUNDED = new Transaction("order3", "android.test.purchased", "com.example", Transaction.PurchaseState.REFUNDED, "notificationId", new Date().getTime(), "developerPayload"); 32 | public static void assertEquals(Transaction a, Transaction b) { 33 | assertTrue(a.equals(b)); 34 | } 35 | 36 | @SmallTest 37 | public void testParseAllFields() throws Exception { 38 | JSONObject json = new JSONObject(); 39 | json.put(Transaction.ORDER_ID, TRANSACTION_1.orderId); 40 | json.put(Transaction.PRODUCT_ID, TRANSACTION_1.productId); 41 | json.put(Transaction.PACKAGE_NAME, TRANSACTION_1.packageName); 42 | json.put(Transaction.PURCHASE_STATE, TRANSACTION_1.purchaseState.ordinal()); 43 | json.put(Transaction.NOTIFICATION_ID, TRANSACTION_1.notificationId); 44 | json.put(Transaction.PURCHASE_TIME, TRANSACTION_1.purchaseTime); 45 | json.put(Transaction.DEVELOPER_PAYLOAD, TRANSACTION_1.developerPayload); 46 | final Transaction parsed = Transaction.parse(json); 47 | assertEquals(TRANSACTION_1, parsed); 48 | } 49 | 50 | @SmallTest 51 | public void testParseOnlyMandatoryFields() throws Exception { 52 | JSONObject json = new JSONObject(); 53 | json.put(Transaction.PRODUCT_ID, TRANSACTION_1.productId); 54 | json.put(Transaction.PACKAGE_NAME, TRANSACTION_1.packageName); 55 | json.put(Transaction.PURCHASE_STATE, TRANSACTION_1.purchaseState.ordinal()); 56 | json.put(Transaction.PURCHASE_TIME, TRANSACTION_1.purchaseTime); 57 | final Transaction parsed = Transaction.parse(json); 58 | assertNull(parsed.orderId); 59 | assertEquals(TRANSACTION_1.productId, parsed.productId); 60 | assertEquals(TRANSACTION_1.packageName, parsed.packageName); 61 | assertEquals(TRANSACTION_1.purchaseState, parsed.purchaseState); 62 | assertNull(parsed.notificationId); 63 | assertEquals(TRANSACTION_1.purchaseTime, parsed.purchaseTime); 64 | assertNull(parsed.developerPayload); 65 | } 66 | 67 | @SmallTest 68 | public void testPurchaseStateOrdinal() throws Exception { 69 | assertEquals(Transaction.PurchaseState.PURCHASED.ordinal(), 0); 70 | assertEquals(Transaction.PurchaseState.CANCELLED.ordinal(), 1); 71 | assertEquals(Transaction.PurchaseState.REFUNDED.ordinal(), 2); 72 | assertEquals(Transaction.PurchaseState.EXPIRED.ordinal(), 3); 73 | } 74 | 75 | @SmallTest 76 | public void testEquals() throws Exception { 77 | assertTrue(TRANSACTION_1.equals(TRANSACTION_1)); 78 | assertTrue(TRANSACTION_1.equals(TRANSACTION_1.clone())); 79 | assertFalse(TRANSACTION_1.equals(TRANSACTION_2_REFUNDED)); 80 | } 81 | 82 | @SmallTest 83 | public void testClone() throws Exception { 84 | assertEquals(TRANSACTION_1, TRANSACTION_1.clone()); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /AndroidBillingLibraryTest/src/net/robotmedia/billing/model/request/ResponseCodeTest.java: -------------------------------------------------------------------------------- 1 | /* Copyright 2011 Robot Media SL (http://www.robotmedia.net) 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 net.robotmedia.billing.model.request; 17 | 18 | import net.robotmedia.billing.BillingRequest.ResponseCode; 19 | import android.test.AndroidTestCase; 20 | import android.test.suitebuilder.annotation.SmallTest; 21 | 22 | public class ResponseCodeTest extends AndroidTestCase { 23 | 24 | @SmallTest 25 | public void testOrdinal() throws Exception { 26 | assertEquals(ResponseCode.RESULT_OK.ordinal(), 0); 27 | assertEquals(ResponseCode.RESULT_USER_CANCELED.ordinal(), 1); 28 | assertEquals(ResponseCode.RESULT_SERVICE_UNAVAILABLE.ordinal(), 2); 29 | assertEquals(ResponseCode.RESULT_BILLING_UNAVAILABLE.ordinal(), 3); 30 | assertEquals(ResponseCode.RESULT_ITEM_UNAVAILABLE.ordinal(), 4); 31 | assertEquals(ResponseCode.RESULT_DEVELOPER_ERROR.ordinal(), 5); 32 | assertEquals(ResponseCode.RESULT_ERROR.ordinal(), 6); 33 | } 34 | 35 | @SmallTest 36 | public void testValueOf() throws Exception { 37 | assertEquals(ResponseCode.RESULT_OK, ResponseCode.valueOf(0)); 38 | assertEquals(ResponseCode.RESULT_USER_CANCELED, ResponseCode.valueOf(1)); 39 | assertEquals(ResponseCode.RESULT_SERVICE_UNAVAILABLE, ResponseCode.valueOf(2)); 40 | assertEquals(ResponseCode.RESULT_BILLING_UNAVAILABLE, ResponseCode.valueOf(3)); 41 | assertEquals(ResponseCode.RESULT_ITEM_UNAVAILABLE, ResponseCode.valueOf(4)); 42 | assertEquals(ResponseCode.RESULT_DEVELOPER_ERROR, ResponseCode.valueOf(5)); 43 | assertEquals(ResponseCode.RESULT_ERROR, ResponseCode.valueOf(6)); 44 | } 45 | 46 | @SmallTest 47 | public void testIsResponseOk() throws Exception { 48 | assertTrue(ResponseCode.isResponseOk(0)); 49 | assertFalse(ResponseCode.isResponseOk(6)); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /AndroidBillingLibraryTest/src/net/robotmedia/billing/utils/CompatibilityTest.java: -------------------------------------------------------------------------------- 1 | package net.robotmedia.billing.utils; 2 | 3 | import android.app.Service; 4 | import android.test.AndroidTestCase; 5 | import android.test.suitebuilder.annotation.SmallTest; 6 | 7 | public class CompatibilityTest extends AndroidTestCase { 8 | 9 | @SmallTest 10 | public void testStartNotSticky() throws Exception { 11 | assertEquals(Compatibility.START_NOT_STICKY, Service.START_NOT_STICKY); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /AndroidBillingLibraryTest/src/net/robotmedia/billing/utils/InstallationTest.java: -------------------------------------------------------------------------------- 1 | /* Copyright 2011 Robot Media SL (http://www.robotmedia.net) 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 net.robotmedia.billing.utils; 17 | 18 | import android.test.AndroidTestCase; 19 | import android.test.suitebuilder.annotation.MediumTest; 20 | import android.text.TextUtils; 21 | 22 | public class InstallationTest extends AndroidTestCase { 23 | 24 | @MediumTest 25 | public void testId() throws Exception { 26 | final String id = Installation.id(getContext()); 27 | assertFalse(TextUtils.isEmpty(id)); 28 | } 29 | 30 | @MediumTest 31 | public void testSameValue() throws Exception { 32 | final String id1 = Installation.id(getContext()); 33 | final String id2 = Installation.id(getContext()); 34 | final String id3 = Installation.id(getContext()); 35 | assertEquals(id1, id2); 36 | assertEquals(id2, id3); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /DungeonsRedux/.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /DungeonsRedux/.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | gen -------------------------------------------------------------------------------- /DungeonsRedux/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | DungeonsRedux 4 | 5 | 6 | 7 | 8 | 9 | com.android.ide.eclipse.adt.ResourceManagerBuilder 10 | 11 | 12 | 13 | 14 | com.android.ide.eclipse.adt.PreCompilerBuilder 15 | 16 | 17 | 18 | 19 | org.eclipse.jdt.core.javabuilder 20 | 21 | 22 | 23 | 24 | com.android.ide.eclipse.adt.ApkBuilder 25 | 26 | 27 | 28 | 29 | 30 | com.android.ide.eclipse.adt.AndroidNature 31 | org.eclipse.jdt.core.javanature 32 | 33 | 34 | -------------------------------------------------------------------------------- /DungeonsRedux/.settings/org.eclipse.jdt.core.prefs: -------------------------------------------------------------------------------- 1 | eclipse.preferences.version=1 2 | org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.5 3 | org.eclipse.jdt.core.compiler.compliance=1.5 4 | org.eclipse.jdt.core.compiler.source=1.5 5 | -------------------------------------------------------------------------------- /DungeonsRedux/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /DungeonsRedux/README: -------------------------------------------------------------------------------- 1 | Dungeons Redux is a sample app that shows how to use the Android Billing Library by Robot Media. 2 | It is a simplified version of the Dungeons in-app billing sample app provided by Google. 3 | 4 | Dungeons Redux does not intend to be an example of how to use in-app billing in general, just 5 | Android Billing Library. 6 | 7 | ------------------------------------- 8 | Configuring the sample application 9 | ------------------------------------- 10 | 11 | Before you can run the sample application, you need to add your Android Market public key to the 12 | sample application code. This enables the application to verify the signature of the transaction 13 | information that's returned from Android Market. To add your public key to the sample application 14 | code, do the following: 15 | 16 | 1. Log in to your Android Market publisher account (http://market.android.com/publish). 17 | 2. On the upper left part of the page, under your name, click Edit Profile. 18 | 3. On the Edit Profile page, scroll down to the Licensing & In-app Billing panel. 19 | 4. Copy your public key to the clipboard. 20 | 5. Open src/net/robotmedia/billing/example/Application.java in the editor of your choice. 21 | 6. Search for the following line: 22 | return "your public key here"; 23 | 7. Replace the string with your public key. 24 | 8. Save the file. 25 | 26 | After you add your public key to the Application.java file, you can compile the application as you 27 | normally would. 28 | 29 | ------------------------------------- 30 | Running the sample application 31 | ------------------------------------- 32 | 33 | You cannot run the sample application in the emulator. You must load the application onto a device 34 | to run it. 35 | 36 | In-app billing requires version 2.3.0 of the Android Market application. To run the sample 37 | application you must have this version (or a newer version) installed on your device. You can check 38 | the version of the Android Market application by doing the following: 39 | 40 | 1. Open Settings on your device and touch Applications. 41 | 2. In Application Settings, touch Manage applications. 42 | 3. Touch All to list all applications. 43 | 4. Scroll down and touch the Market application. 44 | 5. The version number appears under Market at the top of the screen. 45 | 46 | ------------------------------------- 47 | Additional information and resources 48 | ------------------------------------- 49 | 50 | To learn more about the in-app billing service, see the online documentation: 51 | 52 | http://developer.android.com/guide/market/billing/index.html 53 | 54 | To learn more about Android Billing Library, see: 55 | 56 | https://github.com/robotmedia/AndroidBillingLibrary -------------------------------------------------------------------------------- /DungeonsRedux/project.properties: -------------------------------------------------------------------------------- 1 | # This file is automatically generated by Android Tools. 2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED! 3 | # 4 | # This file must be checked in Version Control Systems. 5 | # 6 | # To customize properties used by the Ant build system edit 7 | # "ant.properties", and override values to adapt the script to your 8 | # project structure. 9 | # 10 | # To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): 11 | #proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt 12 | 13 | android.library.reference.1=../AndroidBillingLibrary 14 | # Project target. 15 | target=android-4 16 | -------------------------------------------------------------------------------- /DungeonsRedux/res/drawable-hdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robotmedia/AndroidBillingLibrary/fc637bb2ed9d0c864e1b918f9b604fb140104f6d/DungeonsRedux/res/drawable-hdpi/icon.png -------------------------------------------------------------------------------- /DungeonsRedux/res/drawable-ldpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robotmedia/AndroidBillingLibrary/fc637bb2ed9d0c864e1b918f9b604fb140104f6d/DungeonsRedux/res/drawable-ldpi/icon.png -------------------------------------------------------------------------------- /DungeonsRedux/res/drawable-mdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robotmedia/AndroidBillingLibrary/fc637bb2ed9d0c864e1b918f9b604fb140104f6d/DungeonsRedux/res/drawable-mdpi/icon.png -------------------------------------------------------------------------------- /DungeonsRedux/res/layout/item_row.xml: -------------------------------------------------------------------------------- 1 | 2 | 19 | 20 | 23 | 30 | 31 | -------------------------------------------------------------------------------- /DungeonsRedux/res/layout/main.xml: -------------------------------------------------------------------------------- 1 | 2 | 19 | 20 | 26 | 27 | 32 | 33 | 40 | 41 | 45 |