Jelajahi Sumber

Merge pull request #31709 from akien-mga/android-fix-thirdparty

Android: Fix another regression with Secure.ANDROID_ID, and fix formatting and documentation of thirdparty code
Rémi Verschelde 6 tahun lalu
induk
melakukan
f38c64e8b1
44 mengubah file dengan 5078 tambahan dan 4652 penghapusan
  1. 3 0
      misc/hooks/pre-commit-clang-format
  2. 1 1
      misc/travis/clang-format.sh
  3. 0 47
      platform/android/java/README.md
  4. 39 0
      platform/android/java/THIRDPARTY.md
  5. 169 32
      platform/android/java/aidl/com/android/vending/billing/IInAppBillingService.aidl
  6. 0 2
      platform/android/java/aidl/com/android/vending/licensing/ILicenseResultListener.aidl
  7. 0 2
      platform/android/java/aidl/com/android/vending/licensing/ILicensingService.aidl
  8. 300 0
      platform/android/java/patches/com.google.android.vending.expansion.downloader.patch
  9. 42 0
      platform/android/java/patches/com.google.android.vending.licensing.patch
  10. 84 81
      platform/android/java/src/com/google/android/vending/expansion/downloader/Constants.java
  11. 41 39
      platform/android/java/src/com/google/android/vending/expansion/downloader/DownloadProgressInfo.java
  12. 159 152
      platform/android/java/src/com/google/android/vending/expansion/downloader/DownloaderClientMarshaller.java
  13. 146 138
      platform/android/java/src/com/google/android/vending/expansion/downloader/DownloaderServiceMarshaller.java
  14. 239 227
      platform/android/java/src/com/google/android/vending/expansion/downloader/Helpers.java
  15. 28 28
      platform/android/java/src/com/google/android/vending/expansion/downloader/IDownloaderClient.java
  16. 14 14
      platform/android/java/src/com/google/android/vending/expansion/downloader/IDownloaderService.java
  17. 3 3
      platform/android/java/src/com/google/android/vending/expansion/downloader/IStub.java
  18. 89 86
      platform/android/java/src/com/google/android/vending/expansion/downloader/SystemFacade.java
  19. 66 65
      platform/android/java/src/com/google/android/vending/expansion/downloader/impl/CustomIntentService.java
  20. 56 56
      platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadInfo.java
  21. 174 169
      platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadNotification.java
  22. 710 691
      platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadThread.java
  23. 578 561
      platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloaderService.java
  24. 463 422
      platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadsDB.java
  25. 152 143
      platform/android/java/src/com/google/android/vending/expansion/downloader/impl/HttpDateTime.java
  26. 62 62
      platform/android/java/src/com/google/android/vending/licensing/AESObfuscator.java
  27. 271 270
      platform/android/java/src/com/google/android/vending/licensing/APKExpansionPolicy.java
  28. 2 2
      platform/android/java/src/com/google/android/vending/licensing/DeviceLimiter.java
  29. 0 100
      platform/android/java/src/com/google/android/vending/licensing/ILicenseResultListener.java
  30. 0 100
      platform/android/java/src/com/google/android/vending/licensing/ILicensingService.java
  31. 278 276
      platform/android/java/src/com/google/android/vending/licensing/LicenseChecker.java
  32. 13 13
      platform/android/java/src/com/google/android/vending/licensing/LicenseCheckerCallback.java
  33. 183 184
      platform/android/java/src/com/google/android/vending/licensing/LicenseValidator.java
  34. 3 3
      platform/android/java/src/com/google/android/vending/licensing/NullDeviceLimiter.java
  35. 4 4
      platform/android/java/src/com/google/android/vending/licensing/Obfuscator.java
  36. 13 13
      platform/android/java/src/com/google/android/vending/licensing/Policy.java
  37. 43 41
      platform/android/java/src/com/google/android/vending/licensing/PreferenceObfuscator.java
  38. 42 41
      platform/android/java/src/com/google/android/vending/licensing/ResponseData.java
  39. 168 167
      platform/android/java/src/com/google/android/vending/licensing/ServerManagedPolicy.java
  40. 38 37
      platform/android/java/src/com/google/android/vending/licensing/StrictPolicy.java
  41. 7 7
      platform/android/java/src/com/google/android/vending/licensing/ValidationException.java
  42. 363 341
      platform/android/java/src/com/google/android/vending/licensing/util/Base64.java
  43. 7 7
      platform/android/java/src/com/google/android/vending/licensing/util/Base64DecoderException.java
  44. 25 25
      platform/android/java/src/com/google/android/vending/licensing/util/URIQueryDecoder.java

+ 3 - 0
misc/hooks/pre-commit-clang-format

@@ -86,6 +86,9 @@ do
     if grep -q "thirdparty" <<< $file; then
         continue;
     fi
+    if grep -q "platform/android/java/src/com" <<< $file; then
+        continue;
+    fi
 
     # ignore file if we do check for file extensions and the file
     # does not match any of the extensions specified in $FILE_EXTS

+ 1 - 1
misc/travis/clang-format.sh

@@ -11,7 +11,7 @@ else
     RANGE=HEAD
 fi
 
-FILES=$(git diff-tree --no-commit-id --name-only -r $RANGE | grep -v thirdparty/ | grep -E "\.(c|h|cpp|hpp|cc|hh|cxx|m|mm|inc|java|glsl)$")
+FILES=$(git diff-tree --no-commit-id --name-only -r $RANGE | grep -v thirdparty/ | grep -v platform/android/java/src/com/ | grep -E "\.(c|h|cpp|hpp|cc|hh|cxx|m|mm|inc|java|glsl)$")
 echo "Checking files:\n$FILES"
 
 # create a random filename to store our generated patch

+ 0 - 47
platform/android/java/README.md

@@ -1,47 +0,0 @@
-# Third party libraries
-
-
-## Google's vending library
-
-- Upstream: https://github.com/google/play-licensing/tree/master/lvl_library/src/main/java/com/google/android/vending
-- Version: git (eb57657, 2018) with modifications
-- License: Apache 2.0
-
-Overwrite all files under `com/google/android/vending`
-
-### Modify some files to avoid compile error and lint warning
-
-#### com/google/android/vending/licensing/util/Base64.java
-```
-@@ -338,7 +338,8 @@ public class Base64 {
-                        e += 4;
-                }
- 
--               assert (e == outBuff.length);
-+               if (BuildConfig.DEBUG && e != outBuff.length)
-+                       throw new RuntimeException();
-                return outBuff;
-        }
-```
-
-#### com/google/android/vending/licensing/LicenseChecker.java
-```
-@@ -29,8 +29,8 @@ import android.os.RemoteException;
- import android.provider.Settings.Secure;
- import android.util.Log;
- 
--import com.android.vending.licensing.ILicenseResultListener;
--import com.android.vending.licensing.ILicensingService;
-+import com.google.android.vending.licensing.ILicenseResultListener;
-+import com.google.android.vending.licensing.ILicensingService;
- import com.google.android.vending.licensing.util.Base64;
- import com.google.android.vending.licensing.util.Base64DecoderException;
-```
-```
-@@ -287,13 +287,15 @@ public class LicenseChecker implements ServiceConnection {
-     if (logResponse) {
--        String android_id = Secure.getString(mContext.getContentResolver(),
--                            Secure.ANDROID_ID);
-+        String android_id = Secure.ANDROID_ID;
-         Date date = new Date();
-```

+ 39 - 0
platform/android/java/THIRDPARTY.md

@@ -0,0 +1,39 @@
+# Third-party libraries
+
+This file list third-party libraries used in the Android source folder,
+with their provenance and, when relevant, modifications made to those files.
+
+## com.android.vending.billing
+
+- Upstream: https://github.com/googlesamples/android-play-billing/tree/master/TrivialDrive/app/src/main
+- Version: git (7a94c69, 2019)
+- License: Apache 2.0
+
+Overwrite the file `aidl/com/android/vending/billing/IInAppBillingService.aidl`.
+
+## com.google.android.vending.expansion.downloader
+
+- Upstream: https://github.com/google/play-apk-expansion/tree/master/apkx_library
+- Version: git (9ecf54e, 2017)
+- License: Apache 2.0
+
+Overwrite all files under:
+
+- `src/com/google/android/vending/expansion/downloader`
+
+Some files have been modified for yet unclear reasons.
+See the `patches/com.google.android.vending.expansion.downloader.patch` file.
+
+## com.google.android.vending.licensing
+
+- Upstream: https://github.com/google/play-licensing/tree/master/lvl_library/
+- Version: git (eb57657, 2018) with modifications
+- License: Apache 2.0
+
+Overwrite all files under:
+
+- `aidl/com/android/vending/licensing`
+- `src/com/google/android/vending/licensing`
+
+Some files have been modified to silence linter errors or fix downstream issues.
+See the `patches/com.google.android.vending.licensing.patch` file.

+ 169 - 32
platform/android/java/aidl/com/android/vending/billing/IInAppBillingService.aidl

@@ -34,10 +34,11 @@ import android.os.Bundle;
  *
  * All calls will give a response code with the following possible values
  * RESULT_OK = 0 - success
- * RESULT_USER_CANCELED = 1 - user pressed back or canceled a dialog
- * RESULT_BILLING_UNAVAILABLE = 3 - this billing API version is not supported for the type requested
- * RESULT_ITEM_UNAVAILABLE = 4 - requested SKU is not available for purchase
- * RESULT_DEVELOPER_ERROR = 5 - invalid arguments provided to the API
+ * RESULT_USER_CANCELED = 1 - User pressed back or canceled a dialog
+ * RESULT_SERVICE_UNAVAILABLE = 2 - The network connection is down
+ * RESULT_BILLING_UNAVAILABLE = 3 - This billing API version is not supported for the type requested
+ * RESULT_ITEM_UNAVAILABLE = 4 - Requested SKU is not available for purchase
+ * RESULT_DEVELOPER_ERROR = 5 - Invalid arguments provided to the API
  * RESULT_ERROR = 6 - Fatal error during the API action
  * RESULT_ITEM_ALREADY_OWNED = 7 - Failure to purchase since item is already owned
  * RESULT_ITEM_NOT_OWNED = 8 - Failure to consume since item is not owned
@@ -46,11 +47,11 @@ interface IInAppBillingService {
     /**
      * Checks support for the requested billing API version, package and in-app type.
      * Minimum API version supported by this interface is 3.
-     * @param apiVersion the billing version which the app is using
+     * @param apiVersion billing API version that the app is using
      * @param packageName the package name of the calling app
-     * @param type type of the in-app item being purchased "inapp" for one-time purchases
-     *        and "subs" for subscription.
-     * @return RESULT_OK(0) on success, corresponding result code on failures
+     * @param type type of the in-app item being purchased ("inapp" for one-time purchases
+     *        and "subs" for subscriptions)
+     * @return RESULT_OK(0) on success and appropriate response code on failures.
      */
     int isBillingSupported(int apiVersion, String packageName, String type);
 
@@ -59,16 +60,23 @@ interface IInAppBillingService {
      * Given a list of SKUs of a valid type in the skusBundle, this returns a bundle
      * with a list JSON strings containing the productId, price, title and description.
      * This API can be called with a maximum of 20 SKUs.
-     * @param apiVersion billing API version that the Third-party is using
+     * @param apiVersion billing API version that the app is using
      * @param packageName the package name of the calling app
+     * @param type of the in-app items ("inapp" for one-time purchases
+     *        and "subs" for subscriptions)
      * @param skusBundle bundle containing a StringArrayList of SKUs with key "ITEM_ID_LIST"
      * @return Bundle containing the following key-value pairs
-     *         "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
-     *              failure as listed above.
+     *         "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response codes
+     *                         on failures.
      *         "DETAILS_LIST" with a StringArrayList containing purchase information
-     *              in JSON format similar to:
-     *              '{ "productId" : "exampleSku", "type" : "inapp", "price" : "$5.00",
-     *                 "title : "Example Title", "description" : "This is an example description" }'
+     *                        in JSON format similar to:
+     *                        '{ "productId" : "exampleSku",
+     *                           "type" : "inapp",
+     *                           "price" : "$5.00",
+     *                           "price_currency": "USD",
+     *                           "price_amount_micros": 5000000,
+     *                           "title : "Example Title",
+     *                           "description" : "This is an example description" }'
      */
     Bundle getSkuDetails(int apiVersion, String packageName, String type, in Bundle skusBundle);
 
@@ -78,29 +86,28 @@ interface IInAppBillingService {
      * @param apiVersion billing API version that the app is using
      * @param packageName package name of the calling app
      * @param sku the SKU of the in-app item as published in the developer console
-     * @param type the type of the in-app item ("inapp" for one-time purchases
-     *        and "subs" for subscription).
+     * @param type of the in-app item being purchased ("inapp" for one-time purchases
+     *        and "subs" for subscriptions)
      * @param developerPayload optional argument to be sent back with the purchase information
      * @return Bundle containing the following key-value pairs
-     *         "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
-     *              failure as listed above.
+     *         "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response codes
+     *                         on failures.
      *         "BUY_INTENT" - PendingIntent to start the purchase flow
      *
      * The Pending intent should be launched with startIntentSenderForResult. When purchase flow
      * has completed, the onActivityResult() will give a resultCode of OK or CANCELED.
      * If the purchase is successful, the result data will contain the following key-value pairs
-     *         "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
-     *              failure as listed above.
+     *         "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response
+     *                         codes on failures.
      *         "INAPP_PURCHASE_DATA" - String in JSON format similar to
-     *              '{"orderId":"12999763169054705758.1371079406387615",
-     *                "packageName":"com.example.app",
-     *                "productId":"exampleSku",
-     *                "purchaseTime":1345678900000,
-     *                "purchaseToken" : "122333444455555",
-     *                "developerPayload":"example developer payload" }'
+     *                                 '{"orderId":"12999763169054705758.1371079406387615",
+     *                                   "packageName":"com.example.app",
+     *                                   "productId":"exampleSku",
+     *                                   "purchaseTime":1345678900000,
+     *                                   "purchaseToken" : "122333444455555",
+     *                                   "developerPayload":"example developer payload" }'
      *         "INAPP_DATA_SIGNATURE" - String containing the signature of the purchase data that
      *                                  was signed with the private key of the developer
-     *                                  TODO: change this to app-specific keys.
      */
     Bundle getBuyIntent(int apiVersion, String packageName, String sku, String type,
         String developerPayload);
@@ -112,15 +119,15 @@ interface IInAppBillingService {
      * V1 and V2 that have not been consumed.
      * @param apiVersion billing API version that the app is using
      * @param packageName package name of the calling app
-     * @param type the type of the in-app items being requested
-     *        ("inapp" for one-time purchases and "subs" for subscription).
+     * @param type of the in-app items being requested ("inapp" for one-time purchases
+     *        and "subs" for subscriptions)
      * @param continuationToken to be set as null for the first call, if the number of owned
      *        skus are too many, a continuationToken is returned in the response bundle.
      *        This method can be called again with the continuation token to get the next set of
      *        owned skus.
      * @return Bundle containing the following key-value pairs
-     *         "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
-     *              failure as listed above.
+     *         "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response codes
+                               on failures.
      *         "INAPP_PURCHASE_ITEM_LIST" - StringArrayList containing the list of SKUs
      *         "INAPP_PURCHASE_DATA_LIST" - StringArrayList containing the purchase information
      *         "INAPP_DATA_SIGNATURE_LIST"- StringArrayList containing the signatures
@@ -138,7 +145,137 @@ interface IInAppBillingService {
      * @param packageName package name of the calling app
      * @param purchaseToken token in the purchase information JSON that identifies the purchase
      *        to be consumed
-     * @return 0 if consumption succeeded. Appropriate error values for failures.
+     * @return RESULT_OK(0) if consumption succeeded, appropriate response codes on failures.
      */
     int consumePurchase(int apiVersion, String packageName, String purchaseToken);
+
+    /**
+     * This API is currently under development.
+     */
+    int stub(int apiVersion, String packageName, String type);
+
+    /**
+     * Returns a pending intent to launch the purchase flow for upgrading or downgrading a
+     * subscription. The existing owned SKU(s) should be provided along with the new SKU that
+     * the user is upgrading or downgrading to.
+     * @param apiVersion billing API version that the app is using, must be 5 or later
+     * @param packageName package name of the calling app
+     * @param oldSkus the SKU(s) that the user is upgrading or downgrading from,
+     *        if null or empty this method will behave like {@link #getBuyIntent}
+     * @param newSku the SKU that the user is upgrading or downgrading to
+     * @param type of the item being purchased, currently must be "subs"
+     * @param developerPayload optional argument to be sent back with the purchase information
+     * @return Bundle containing the following key-value pairs
+     *         "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response codes
+     *                         on failures.
+     *         "BUY_INTENT" - PendingIntent to start the purchase flow
+     *
+     * The Pending intent should be launched with startIntentSenderForResult. When purchase flow
+     * has completed, the onActivityResult() will give a resultCode of OK or CANCELED.
+     * If the purchase is successful, the result data will contain the following key-value pairs
+     *         "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response
+     *                         codes on failures.
+     *         "INAPP_PURCHASE_DATA" - String in JSON format similar to
+     *                                 '{"orderId":"12999763169054705758.1371079406387615",
+     *                                   "packageName":"com.example.app",
+     *                                   "productId":"exampleSku",
+     *                                   "purchaseTime":1345678900000,
+     *                                   "purchaseToken" : "122333444455555",
+     *                                   "developerPayload":"example developer payload" }'
+     *         "INAPP_DATA_SIGNATURE" - String containing the signature of the purchase data that
+     *                                  was signed with the private key of the developer
+     */
+    Bundle getBuyIntentToReplaceSkus(int apiVersion, String packageName,
+        in List<String> oldSkus, String newSku, String type, String developerPayload);
+
+    /**
+     * Returns a pending intent to launch the purchase flow for an in-app item. This method is
+     * a variant of the {@link #getBuyIntent} method and takes an additional {@code extraParams}
+     * parameter. This parameter is a Bundle of optional keys and values that affect the
+     * operation of the method.
+     * @param apiVersion billing API version that the app is using, must be 6 or later
+     * @param packageName package name of the calling app
+     * @param sku the SKU of the in-app item as published in the developer console
+     * @param type of the in-app item being purchased ("inapp" for one-time purchases
+     *        and "subs" for subscriptions)
+     * @param developerPayload optional argument to be sent back with the purchase information
+     * @extraParams a Bundle with the following optional keys:
+     *        "skusToReplace" - List<String> - an optional list of SKUs that the user is
+     *                          upgrading or downgrading from.
+     *                          Pass this field if the purchase is upgrading or downgrading
+     *                          existing subscriptions.
+     *                          The specified SKUs are replaced with the SKUs that the user is
+     *                          purchasing. Google Play replaces the specified SKUs at the start of
+     *                          the next billing cycle.
+     * "replaceSkusProration" - Boolean - whether the user should be credited for any unused
+     *                          subscription time on the SKUs they are upgrading or downgrading.
+     *                          If you set this field to true, Google Play swaps out the old SKUs
+     *                          and credits the user with the unused value of their subscription
+     *                          time on a pro-rated basis.
+     *                          Google Play applies this credit to the new subscription, and does
+     *                          not begin billing the user for the new subscription until after
+     *                          the credit is used up.
+     *                          If you set this field to false, the user does not receive credit for
+     *                          any unused subscription time and the recurrence date does not
+     *                          change.
+     *                          Default value is true. Ignored if you do not pass skusToReplace.
+     *            "accountId" - String - an optional obfuscated string that is uniquely
+     *                          associated with the user's account in your app.
+     *                          If you pass this value, Google Play can use it to detect irregular
+     *                          activity, such as many devices making purchases on the same
+     *                          account in a short period of time.
+     *                          Do not use the developer ID or the user's Google ID for this field.
+     *                          In addition, this field should not contain the user's ID in
+     *                          cleartext.
+     *                          We recommend that you use a one-way hash to generate a string from
+     *                          the user's ID, and store the hashed string in this field.
+     *                   "vr" - Boolean - an optional flag indicating whether the returned intent
+     *                          should start a VR purchase flow. The apiVersion must also be 7 or
+     *                          later to use this flag.
+     */
+    Bundle getBuyIntentExtraParams(int apiVersion, String packageName, String sku,
+        String type, String developerPayload, in Bundle extraParams);
+
+    /**
+     * Returns the most recent purchase made by the user for each SKU, even if that purchase is
+     * expired, canceled, or consumed.
+     * @param apiVersion billing API version that the app is using, must be 6 or later
+     * @param packageName package name of the calling app
+     * @param type of the in-app items being requested ("inapp" for one-time purchases
+     *        and "subs" for subscriptions)
+     * @param continuationToken to be set as null for the first call, if the number of owned
+     *        skus is too large, a continuationToken is returned in the response bundle.
+     *        This method can be called again with the continuation token to get the next set of
+     *        owned skus.
+     * @param extraParams a Bundle with extra params that would be appended into http request
+     *        query string. Not used at this moment. Reserved for future functionality.
+     * @return Bundle containing the following key-value pairs
+     *         "RESPONSE_CODE" with int value: RESULT_OK(0) if success,
+     *         {@link IabHelper#BILLING_RESPONSE_RESULT_*} response codes on failures.
+     *
+     *         "INAPP_PURCHASE_ITEM_LIST" - ArrayList<String> containing the list of SKUs
+     *         "INAPP_PURCHASE_DATA_LIST" - ArrayList<String> containing the purchase information
+     *         "INAPP_DATA_SIGNATURE_LIST"- ArrayList<String> containing the signatures
+     *                                      of the purchase information
+     *         "INAPP_CONTINUATION_TOKEN" - String containing a continuation token for the
+     *                                      next set of in-app purchases. Only set if the
+     *                                      user has more owned skus than the current list.
+     */
+    Bundle getPurchaseHistory(int apiVersion, String packageName, String type,
+        String continuationToken, in Bundle extraParams);
+
+    /**
+    * This method is a variant of {@link #isBillingSupported}} that takes an additional
+    * {@code extraParams} parameter.
+    * @param apiVersion billing API version that the app is using, must be 7 or later
+    * @param packageName package name of the calling app
+    * @param type of the in-app item being purchased ("inapp" for one-time purchases and "subs"
+    *        for subscriptions)
+    * @param extraParams a Bundle with the following optional keys:
+    *        "vr" - Boolean - an optional flag to indicate whether {link #getBuyIntentExtraParams}
+    *               supports returning a VR purchase flow.
+    * @return RESULT_OK(0) on success and appropriate response code on failures.
+    */
+    int isBillingSupportedExtraParams(int apiVersion, String packageName, String type,
+        in Bundle extraParams);
 }

+ 0 - 2
platform/android/java/src/com/google/android/vending/licensing/ILicenseResultListener.aidl → platform/android/java/aidl/com/android/vending/licensing/ILicenseResultListener.aidl

@@ -16,8 +16,6 @@
 
 package com.android.vending.licensing;
 
-// Android library projects do not yet support AIDL, so this has been
-// precompiled into the src directory.
 oneway interface ILicenseResultListener {
   void verifyLicense(int responseCode, String signedData, String signature);
 }

+ 0 - 2
platform/android/java/src/com/google/android/vending/licensing/ILicensingService.aidl → platform/android/java/aidl/com/android/vending/licensing/ILicensingService.aidl

@@ -18,8 +18,6 @@ package com.android.vending.licensing;
 
 import com.android.vending.licensing.ILicenseResultListener;
 
-// Android library projects do not yet support AIDL, so this has been
-// precompiled into the src directory.
 oneway interface ILicensingService {
   void checkLicense(long nonce, String packageName, in ILicenseResultListener listener);
 }

+ 300 - 0
platform/android/java/patches/com.google.android.vending.expansion.downloader.patch

@@ -0,0 +1,300 @@
+diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/DownloaderClientMarshaller.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/DownloaderClientMarshaller.java
+index ad6ea0de6..452c7d148 100644
+--- a/platform/android/java/src/com/google/android/vending/expansion/downloader/DownloaderClientMarshaller.java
++++ b/platform/android/java/src/com/google/android/vending/expansion/downloader/DownloaderClientMarshaller.java
+@@ -32,6 +32,9 @@ import android.os.Messenger;
+ import android.os.RemoteException;
+ import android.util.Log;
+ 
++// -- GODOT start --
++import java.lang.ref.WeakReference;
++// -- GODOT end --
+ 
+ 
+ /**
+@@ -118,29 +121,46 @@ public class DownloaderClientMarshaller {
+         /**
+          * Target we publish for clients to send messages to IncomingHandler.
+          */
+-        final Messenger mMessenger = new Messenger(new Handler() {
++        // -- GODOT start --
++        private final MessengerHandlerClient mMsgHandler = new MessengerHandlerClient(this);
++        final Messenger mMessenger = new Messenger(mMsgHandler);
++
++        private static class MessengerHandlerClient extends Handler {
++            private final WeakReference<Stub> mDownloader;
++            public MessengerHandlerClient(Stub downloader) {
++                mDownloader = new WeakReference<>(downloader);
++            }
++
+             @Override
+             public void handleMessage(Message msg) {
+-                switch (msg.what) {
+-                    case MSG_ONDOWNLOADPROGRESS:
+-                        Bundle bun = msg.getData();
+-                        if ( null != mContext ) {
+-                            bun.setClassLoader(mContext.getClassLoader());
+-                            DownloadProgressInfo dpi = (DownloadProgressInfo) msg.getData()
+-                                    .getParcelable(PARAM_PROGRESS);
+-                            mItf.onDownloadProgress(dpi);
+-                        }
+-                        break;
+-                    case MSG_ONDOWNLOADSTATE_CHANGED:
+-                        mItf.onDownloadStateChanged(msg.getData().getInt(PARAM_NEW_STATE));
+-                        break;
+-                    case MSG_ONSERVICECONNECTED:
+-                        mItf.onServiceConnected(
+-                                (Messenger) msg.getData().getParcelable(PARAM_MESSENGER));
+-                        break;
++                Stub downloader = mDownloader.get();
++                if (downloader != null) {
++                    downloader.handleMessage(msg);
+                 }
+             }
+-        });
++        }
++
++        private void handleMessage(Message msg) {
++            switch (msg.what) {
++                case MSG_ONDOWNLOADPROGRESS:
++                    Bundle bun = msg.getData();
++                    if (null != mContext) {
++                        bun.setClassLoader(mContext.getClassLoader());
++                        DownloadProgressInfo dpi = (DownloadProgressInfo)msg.getData()
++                                                            .getParcelable(PARAM_PROGRESS);
++                        mItf.onDownloadProgress(dpi);
++                    }
++                    break;
++                case MSG_ONDOWNLOADSTATE_CHANGED:
++                    mItf.onDownloadStateChanged(msg.getData().getInt(PARAM_NEW_STATE));
++                    break;
++                case MSG_ONSERVICECONNECTED:
++                    mItf.onServiceConnected(
++                            (Messenger)msg.getData().getParcelable(PARAM_MESSENGER));
++                    break;
++            }
++        }
++        // -- GODOT end --
+ 
+         public Stub(IDownloaderClient itf, Class<?> downloaderService) {
+             mItf = itf;
+diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/DownloaderServiceMarshaller.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/DownloaderServiceMarshaller.java
+index 979352299..3771d19c9 100644
+--- a/platform/android/java/src/com/google/android/vending/expansion/downloader/DownloaderServiceMarshaller.java
++++ b/platform/android/java/src/com/google/android/vending/expansion/downloader/DownloaderServiceMarshaller.java
+@@ -25,6 +25,9 @@ import android.os.Message;
+ import android.os.Messenger;
+ import android.os.RemoteException;
+ 
++// -- GODOT start --
++import java.lang.ref.WeakReference;
++// -- GODOT end --
+ 
+ 
+ /**
+@@ -108,32 +111,49 @@ public class DownloaderServiceMarshaller {
+ 
+     private static class Stub implements IStub {
+         private IDownloaderService mItf = null;
+-        final Messenger mMessenger = new Messenger(new Handler() {
++        // -- GODOT start --
++        private final MessengerHandlerServer mMsgHandler = new MessengerHandlerServer(this);
++        final Messenger mMessenger = new Messenger(mMsgHandler);
++
++        private static class MessengerHandlerServer extends Handler {
++            private final WeakReference<Stub> mDownloader;
++            public MessengerHandlerServer(Stub downloader) {
++                mDownloader = new WeakReference<>(downloader);
++            }
++
+             @Override
+             public void handleMessage(Message msg) {
+-                switch (msg.what) {
+-                    case MSG_REQUEST_ABORT_DOWNLOAD:
+-                        mItf.requestAbortDownload();
+-                        break;
+-                    case MSG_REQUEST_CONTINUE_DOWNLOAD:
+-                        mItf.requestContinueDownload();
+-                        break;
+-                    case MSG_REQUEST_PAUSE_DOWNLOAD:
+-                        mItf.requestPauseDownload();
+-                        break;
+-                    case MSG_SET_DOWNLOAD_FLAGS:
+-                        mItf.setDownloadFlags(msg.getData().getInt(PARAMS_FLAGS));
+-                        break;
+-                    case MSG_REQUEST_DOWNLOAD_STATE:
+-                        mItf.requestDownloadStatus();
+-                        break;
+-                    case MSG_REQUEST_CLIENT_UPDATE:
+-                        mItf.onClientUpdated((Messenger) msg.getData().getParcelable(
+-                                PARAM_MESSENGER));
+-                        break;
++                Stub downloader = mDownloader.get();
++                if (downloader != null) {
++                    downloader.handleMessage(msg);
+                 }
+             }
+-        });
++        }
++
++        private void handleMessage(Message msg) {
++            switch (msg.what) {
++                case MSG_REQUEST_ABORT_DOWNLOAD:
++                    mItf.requestAbortDownload();
++                    break;
++                case MSG_REQUEST_CONTINUE_DOWNLOAD:
++                    mItf.requestContinueDownload();
++                    break;
++                case MSG_REQUEST_PAUSE_DOWNLOAD:
++                    mItf.requestPauseDownload();
++                    break;
++                case MSG_SET_DOWNLOAD_FLAGS:
++                    mItf.setDownloadFlags(msg.getData().getInt(PARAMS_FLAGS));
++                    break;
++                case MSG_REQUEST_DOWNLOAD_STATE:
++                    mItf.requestDownloadStatus();
++                    break;
++                case MSG_REQUEST_CLIENT_UPDATE:
++                    mItf.onClientUpdated((Messenger)msg.getData().getParcelable(
++                            PARAM_MESSENGER));
++                    break;
++            }
++        }
++        // -- GODOT end --
+ 
+         public Stub(IDownloaderService itf) {
+             mItf = itf;
+diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/Helpers.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/Helpers.java
+index e4b1b0f1c..36cd6aacf 100644
+--- a/platform/android/java/src/com/google/android/vending/expansion/downloader/Helpers.java
++++ b/platform/android/java/src/com/google/android/vending/expansion/downloader/Helpers.java
+@@ -24,7 +24,10 @@ import android.os.StatFs;
+ import android.os.SystemClock;
+ import android.util.Log;
+ 
+-import com.android.vending.expansion.downloader.R;
++// -- GODOT start --
++//import com.android.vending.expansion.downloader.R;
++import com.godot.game.R;
++// -- GODOT end --
+ 
+ import java.io.File;
+ import java.text.SimpleDateFormat;
+@@ -146,12 +149,14 @@ public class Helpers {
+             }
+             return "";
+         }
+-        return String.format("%.2f",
++        // -- GODOT start --
++        return String.format(Locale.ENGLISH, "%.2f",
+                 (float) overallProgress / (1024.0f * 1024.0f))
+                 + "MB /" +
+-                String.format("%.2f", (float) overallTotal /
++                String.format(Locale.ENGLISH, "%.2f", (float) overallTotal /
+                         (1024.0f * 1024.0f))
+                 + "MB";
++        // -- GODOT end --
+     }
+ 
+     /**
+@@ -184,7 +189,9 @@ public class Helpers {
+     }
+ 
+     public static String getSpeedString(float bytesPerMillisecond) {
+-        return String.format("%.2f", bytesPerMillisecond * 1000 / 1024);
++        // -- GODOT start --
++        return String.format(Locale.ENGLISH, "%.2f", bytesPerMillisecond * 1000 / 1024);
++        // -- GODOT end --
+     }
+ 
+     public static String getTimeRemaining(long durationInMilliseconds) {
+diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/SystemFacade.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/SystemFacade.java
+index 12edd97ab..a0e1165cc 100644
+--- a/platform/android/java/src/com/google/android/vending/expansion/downloader/SystemFacade.java
++++ b/platform/android/java/src/com/google/android/vending/expansion/downloader/SystemFacade.java
+@@ -26,6 +26,10 @@ import android.net.NetworkInfo;
+ import android.telephony.TelephonyManager;
+ import android.util.Log;
+ 
++// -- GODOT start --
++import android.annotation.SuppressLint;
++// -- GODOT end --
++
+ /**
+  * Contains useful helper functions, typically tied to the application context.
+  */
+@@ -51,6 +55,7 @@ class SystemFacade {
+             return null;
+         }
+ 
++        @SuppressLint("MissingPermission")
+         NetworkInfo activeInfo = connectivity.getActiveNetworkInfo();
+         if (activeInfo == null) {
+             if (Constants.LOGVV) {
+@@ -69,6 +74,7 @@ class SystemFacade {
+             return false;
+         }
+ 
++        @SuppressLint("MissingPermission")
+         NetworkInfo info = connectivity.getActiveNetworkInfo();
+         boolean isMobile = (info != null && info.getType() == ConnectivityManager.TYPE_MOBILE);
+         TelephonyManager tm = (TelephonyManager) mContext
+diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadNotification.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadNotification.java
+index f1536e80e..4b214b22d 100644
+--- a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadNotification.java
++++ b/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadNotification.java
+@@ -16,7 +16,11 @@
+ 
+ package com.google.android.vending.expansion.downloader.impl;
+ 
+-import com.android.vending.expansion.downloader.R;
++// -- GODOT start --
++//import com.android.vending.expansion.downloader.R;
++import com.godot.game.R;
++// -- GODOT end --
++
+ import com.google.android.vending.expansion.downloader.DownloadProgressInfo;
+ import com.google.android.vending.expansion.downloader.DownloaderClientMarshaller;
+ import com.google.android.vending.expansion.downloader.Helpers;
+diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadThread.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadThread.java
+index b2e0e7af0..c114b8a64 100644
+--- a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadThread.java
++++ b/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadThread.java
+@@ -146,8 +146,12 @@ public class DownloadThread {
+ 
+         try {
+             PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
+-            wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, Constants.TAG);
+-            wakeLock.acquire();
++            // -- GODOT start --
++            //wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, Constants.TAG);
++            //wakeLock.acquire();
++            wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "org.godot.game:wakelock");
++            wakeLock.acquire(20 * 60 * 1000L /*20 minutes*/);
++            // -- GODOT end --
+ 
+             if (Constants.LOGV) {
+                 Log.v(Constants.TAG, "initiating download for " + mInfo.mFileName);
+diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloaderService.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloaderService.java
+index 4babe476f..8d41a7690 100644
+--- a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloaderService.java
++++ b/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloaderService.java
+@@ -50,6 +50,10 @@ import android.provider.Settings.Secure;
+ import android.telephony.TelephonyManager;
+ import android.util.Log;
+ 
++// -- GODOT start --
++import android.annotation.SuppressLint;
++// -- GODOT end --
++
+ import java.io.File;
+ 
+ /**
+@@ -578,6 +582,7 @@ public abstract class DownloaderService extends CustomIntentService implements I
+             Log.w(Constants.TAG,
+                     "couldn't get connectivity manager to poll network state");
+         } else {
++            @SuppressLint("MissingPermission")
+             NetworkInfo activeInfo = mConnectivityManager
+                     .getActiveNetworkInfo();
+             updateNetworkState(activeInfo);

+ 42 - 0
platform/android/java/patches/com.google.android.vending.licensing.patch

@@ -0,0 +1,42 @@
+diff --git a/platform/android/java/src/com/google/android/vending/licensing/PreferenceObfuscator.java b/platform/android/java/src/com/google/android/vending/licensing/PreferenceObfuscator.java
+index 7c42bfc28..feb579af0 100644
+--- a/platform/android/java/src/com/google/android/vending/licensing/PreferenceObfuscator.java
++++ b/platform/android/java/src/com/google/android/vending/licensing/PreferenceObfuscator.java
+@@ -45,6 +45,9 @@ public class PreferenceObfuscator {
+     public void putString(String key, String value) {
+         if (mEditor == null) {
+             mEditor = mPreferences.edit();
++            // -- GODOT start --
++            mEditor.apply();
++            // -- GODOT end --
+         }
+         String obfuscatedValue = mObfuscator.obfuscate(value, key);
+         mEditor.putString(key, obfuscatedValue);
+diff --git a/platform/android/java/src/com/google/android/vending/licensing/util/Base64.java b/platform/android/java/src/com/google/android/vending/licensing/util/Base64.java
+index a0d2779af..a8bf65f9c 100644
+--- a/platform/android/java/src/com/google/android/vending/licensing/util/Base64.java
++++ b/platform/android/java/src/com/google/android/vending/licensing/util/Base64.java
+@@ -31,6 +31,10 @@ package com.google.android.vending.licensing.util;
+  * @version 1.3
+  */
+ 
++// -- GODOT start --
++import com.godot.game.BuildConfig;
++// -- GODOT end --
++
+ /**
+  * Base64 converter class. This code is not a full-blown MIME encoder;
+  * it simply converts binary data to base64 data and back.
+@@ -341,7 +345,11 @@ public class Base64 {
+       e += 4;
+     }
+ 
+-    assert (e == outBuff.length);
++    // -- GODOT start --
++    //assert (e == outBuff.length);
++    if (BuildConfig.DEBUG && e != outBuff.length)
++      throw new RuntimeException();
++    // -- GODOT end --
+     return outBuff;
+   }
+ 

+ 84 - 81
platform/android/java/src/com/google/android/vending/expansion/downloader/Constants.java

@@ -18,113 +18,115 @@ package com.google.android.vending.expansion.downloader;
 
 import java.io.File;
 
+
 /**
  * Contains the internal constants that are used in the download manager.
  * As a general rule, modifying these constants should be done with care.
  */
 public class Constants {
-	/** Tag used for debugging/logging */
-	public static final String TAG = "LVLDL";
+    /** Tag used for debugging/logging */
+    public static final String TAG = "LVLDL";
 
-	/**
+    /**
      * Expansion path where we store obb files
      */
-	public static final String EXP_PATH = File.separator + "Android" + File.separator + "obb" + File.separator;
+    public static final String EXP_PATH = File.separator + "Android"
+            + File.separator + "obb" + File.separator;
 
-	/** The intent that gets sent when the service must wake up for a retry */
-	public static final String ACTION_RETRY = "android.intent.action.DOWNLOAD_WAKEUP";
+    /** The intent that gets sent when the service must wake up for a retry */
+    public static final String ACTION_RETRY = "android.intent.action.DOWNLOAD_WAKEUP";
 
-	/** the intent that gets sent when clicking a successful download */
-	public static final String ACTION_OPEN = "android.intent.action.DOWNLOAD_OPEN";
+    /** the intent that gets sent when clicking a successful download */
+    public static final String ACTION_OPEN = "android.intent.action.DOWNLOAD_OPEN";
 
-	/** the intent that gets sent when clicking an incomplete/failed download  */
-	public static final String ACTION_LIST = "android.intent.action.DOWNLOAD_LIST";
+    /** the intent that gets sent when clicking an incomplete/failed download  */
+    public static final String ACTION_LIST = "android.intent.action.DOWNLOAD_LIST";
 
-	/** the intent that gets sent when deleting the notification of a completed download */
-	public static final String ACTION_HIDE = "android.intent.action.DOWNLOAD_HIDE";
+    /** the intent that gets sent when deleting the notification of a completed download */
+    public static final String ACTION_HIDE = "android.intent.action.DOWNLOAD_HIDE";
 
-	/**
+    /**
      * When a number has to be appended to the filename, this string is used to separate the
      * base filename from the sequence number
      */
-	public static final String FILENAME_SEQUENCE_SEPARATOR = "-";
+    public static final String FILENAME_SEQUENCE_SEPARATOR = "-";
 
-	/** The default user agent used for downloads */
-	public static final String DEFAULT_USER_AGENT = "Android.LVLDM";
+    /** The default user agent used for downloads */
+    public static final String DEFAULT_USER_AGENT = "Android.LVLDM";
 
-	/** The buffer size used to stream the data */
-	public static final int BUFFER_SIZE = 4096;
+    /** The buffer size used to stream the data */
+    public static final int BUFFER_SIZE = 4096;
 
-	/** The minimum amount of progress that has to be done before the progress bar gets updated */
-	public static final int MIN_PROGRESS_STEP = 4096;
+    /** The minimum amount of progress that has to be done before the progress bar gets updated */
+    public static final int MIN_PROGRESS_STEP = 4096;
 
-	/** The minimum amount of time that has to elapse before the progress bar gets updated, in ms */
-	public static final long MIN_PROGRESS_TIME = 1000;
+    /** The minimum amount of time that has to elapse before the progress bar gets updated, in ms */
+    public static final long MIN_PROGRESS_TIME = 1000;
 
-	/** The maximum number of rows in the database (FIFO) */
-	public static final int MAX_DOWNLOADS = 1000;
+    /** The maximum number of rows in the database (FIFO) */
+    public static final int MAX_DOWNLOADS = 1000;
 
-	/**
+    /**
      * The number of times that the download manager will retry its network
      * operations when no progress is happening before it gives up.
      */
-	public static final int MAX_RETRIES = 5;
+    public static final int MAX_RETRIES = 5;
 
-	/**
+    /**
      * The minimum amount of time that the download manager accepts for
      * a Retry-After response header with a parameter in delta-seconds.
      */
-	public static final int MIN_RETRY_AFTER = 30; // 30s
+    public static final int MIN_RETRY_AFTER = 30; // 30s
 
-	/**
+    /**
      * The maximum amount of time that the download manager accepts for
      * a Retry-After response header with a parameter in delta-seconds.
      */
-	public static final int MAX_RETRY_AFTER = 24 * 60 * 60; // 24h
+    public static final int MAX_RETRY_AFTER = 24 * 60 * 60; // 24h
 
-	/**
+    /**
      * The maximum number of redirects.
      */
-	public static final int MAX_REDIRECTS = 5; // can't be more than 7.
+    public static final int MAX_REDIRECTS = 5; // can't be more than 7.
 
-	/**
+    /**
      * The time between a failure and the first retry after an IOException.
      * Each subsequent retry grows exponentially, doubling each time.
      * The time is in seconds.
      */
-	public static final int RETRY_FIRST_DELAY = 30;
+    public static final int RETRY_FIRST_DELAY = 30;
 
-	/** Enable separate connectivity logging */
-	public static final boolean LOGX = true;
+    /** Enable separate connectivity logging */
+    public static final boolean LOGX = true;
 
-	/** Enable verbose logging */
-	public static final boolean LOGV = false;
+    /** Enable verbose logging */
+    public static final boolean LOGV = false;
 
-	/** Enable super-verbose logging */
-	private static final boolean LOCAL_LOGVV = false;
-	public static final boolean LOGVV = LOCAL_LOGVV && LOGV;
+    /** Enable super-verbose logging */
+    private static final boolean LOCAL_LOGVV = false;
+    public static final boolean LOGVV = LOCAL_LOGVV && LOGV;
 
-	/**
+    /**
      * This download has successfully completed.
      * Warning: there might be other status values that indicate success
      * in the future.
      * Use isSucccess() to capture the entire category.
      */
-	public static final int STATUS_SUCCESS = 200;
+    public static final int STATUS_SUCCESS = 200;
 
-	/**
+    /**
      * This request couldn't be parsed. This is also used when processing
      * requests with unknown/unsupported URI schemes.
      */
-	public static final int STATUS_BAD_REQUEST = 400;
+    public static final int STATUS_BAD_REQUEST = 400;
 
-	/**
+    /**
      * This download can't be performed because the content type cannot be
      * handled.
      */
-	public static final int STATUS_NOT_ACCEPTABLE = 406;
+    public static final int STATUS_NOT_ACCEPTABLE = 406;
 
-	/**
+    /**
      * This download cannot be performed because the length cannot be
      * determined accurately. This is the code for the HTTP error "Length
      * Required", which is typically used when making requests that require
@@ -133,101 +135,102 @@ public class Constants {
      * accurately (therefore making it impossible to know when a download
      * completes).
      */
-	public static final int STATUS_LENGTH_REQUIRED = 411;
+    public static final int STATUS_LENGTH_REQUIRED = 411;
 
-	/**
+    /**
      * This download was interrupted and cannot be resumed.
      * This is the code for the HTTP error "Precondition Failed", and it is
      * also used in situations where the client doesn't have an ETag at all.
      */
-	public static final int STATUS_PRECONDITION_FAILED = 412;
+    public static final int STATUS_PRECONDITION_FAILED = 412;
 
-	/**
+    /**
      * The lowest-valued error status that is not an actual HTTP status code.
      */
-	public static final int MIN_ARTIFICIAL_ERROR_STATUS = 488;
+    public static final int MIN_ARTIFICIAL_ERROR_STATUS = 488;
 
-	/**
+    /**
      * The requested destination file already exists.
      */
-	public static final int STATUS_FILE_ALREADY_EXISTS_ERROR = 488;
+    public static final int STATUS_FILE_ALREADY_EXISTS_ERROR = 488;
 
-	/**
+    /**
      * Some possibly transient error occurred, but we can't resume the download.
      */
-	public static final int STATUS_CANNOT_RESUME = 489;
+    public static final int STATUS_CANNOT_RESUME = 489;
 
-	/**
+    /**
      * This download was canceled
      */
-	public static final int STATUS_CANCELED = 490;
+    public static final int STATUS_CANCELED = 490;
 
-	/**
+    /**
      * This download has completed with an error.
      * Warning: there will be other status values that indicate errors in
      * the future. Use isStatusError() to capture the entire category.
      */
-	public static final int STATUS_UNKNOWN_ERROR = 491;
+    public static final int STATUS_UNKNOWN_ERROR = 491;
 
-	/**
+    /**
      * This download couldn't be completed because of a storage issue.
      * Typically, that's because the filesystem is missing or full.
      * Use the more specific {@link #STATUS_INSUFFICIENT_SPACE_ERROR}
      * and {@link #STATUS_DEVICE_NOT_FOUND_ERROR} when appropriate.
      */
-	public static final int STATUS_FILE_ERROR = 492;
+    public static final int STATUS_FILE_ERROR = 492;
 
-	/**
+    /**
      * This download couldn't be completed because of an HTTP
      * redirect response that the download manager couldn't
      * handle.
      */
-	public static final int STATUS_UNHANDLED_REDIRECT = 493;
+    public static final int STATUS_UNHANDLED_REDIRECT = 493;
 
-	/**
+    /**
      * This download couldn't be completed because of an
      * unspecified unhandled HTTP code.
      */
-	public static final int STATUS_UNHANDLED_HTTP_CODE = 494;
+    public static final int STATUS_UNHANDLED_HTTP_CODE = 494;
 
-	/**
+    /**
      * This download couldn't be completed because of an
      * error receiving or processing data at the HTTP level.
      */
-	public static final int STATUS_HTTP_DATA_ERROR = 495;
+    public static final int STATUS_HTTP_DATA_ERROR = 495;
 
-	/**
+    /**
      * This download couldn't be completed because of an
      * HttpException while setting up the request.
      */
-	public static final int STATUS_HTTP_EXCEPTION = 496;
+    public static final int STATUS_HTTP_EXCEPTION = 496;
 
-	/**
+    /**
      * This download couldn't be completed because there were
      * too many redirects.
      */
-	public static final int STATUS_TOO_MANY_REDIRECTS = 497;
+    public static final int STATUS_TOO_MANY_REDIRECTS = 497;
 
-	/**
+    /**
      * This download couldn't be completed due to insufficient storage
      * space.  Typically, this is because the SD card is full.
      */
-	public static final int STATUS_INSUFFICIENT_SPACE_ERROR = 498;
+    public static final int STATUS_INSUFFICIENT_SPACE_ERROR = 498;
 
-	/**
+    /**
      * This download couldn't be completed because no external storage
      * device was found.  Typically, this is because the SD card is not
      * mounted.
      */
-	public static final int STATUS_DEVICE_NOT_FOUND_ERROR = 499;
+    public static final int STATUS_DEVICE_NOT_FOUND_ERROR = 499;
 
-	/**
+    /**
      * The wake duration to check to see if a download is possible.
      */
-	public static final long WATCHDOG_WAKE_TIMER = 60 * 1000;
+    public static final long WATCHDOG_WAKE_TIMER = 60*1000;
 
-	/**
+    /**
      * The wake duration to check to see if the process was killed.
      */
-	public static final long ACTIVE_THREAD_WATCHDOG = 5 * 1000;
+    public static final long ACTIVE_THREAD_WATCHDOG = 5*1000;
+
 }

+ 41 - 39
platform/android/java/src/com/google/android/vending/expansion/downloader/DownloadProgressInfo.java

@@ -19,6 +19,7 @@ package com.google.android.vending.expansion.downloader;
 import android.os.Parcel;
 import android.os.Parcelable;
 
+
 /**
  * This class contains progress information about the active download(s).
  *
@@ -30,49 +31,50 @@ import android.os.Parcelable;
  * as the progress so far, time remaining and current speed.
  */
 public class DownloadProgressInfo implements Parcelable {
-	public long mOverallTotal;
-	public long mOverallProgress;
-	public long mTimeRemaining; // time remaining
-	public float mCurrentSpeed; // speed in KB/S
+    public long mOverallTotal;
+    public long mOverallProgress;
+    public long mTimeRemaining; // time remaining
+    public float mCurrentSpeed; // speed in KB/S
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
 
-	@Override
-	public int describeContents() {
-		return 0;
-	}
+    @Override
+    public void writeToParcel(Parcel p, int i) {
+        p.writeLong(mOverallTotal);
+        p.writeLong(mOverallProgress);
+        p.writeLong(mTimeRemaining);
+        p.writeFloat(mCurrentSpeed);
+    }
 
-	@Override
-	public void writeToParcel(Parcel p, int i) {
-		p.writeLong(mOverallTotal);
-		p.writeLong(mOverallProgress);
-		p.writeLong(mTimeRemaining);
-		p.writeFloat(mCurrentSpeed);
-	}
+    public DownloadProgressInfo(Parcel p) {
+        mOverallTotal = p.readLong();
+        mOverallProgress = p.readLong();
+        mTimeRemaining = p.readLong();
+        mCurrentSpeed = p.readFloat();
+    }
 
-	public DownloadProgressInfo(Parcel p) {
-		mOverallTotal = p.readLong();
-		mOverallProgress = p.readLong();
-		mTimeRemaining = p.readLong();
-		mCurrentSpeed = p.readFloat();
-	}
+    public DownloadProgressInfo(long overallTotal, long overallProgress,
+            long timeRemaining,
+            float currentSpeed) {
+        this.mOverallTotal = overallTotal;
+        this.mOverallProgress = overallProgress;
+        this.mTimeRemaining = timeRemaining;
+        this.mCurrentSpeed = currentSpeed;
+    }
 
-	public DownloadProgressInfo(long overallTotal, long overallProgress,
-			long timeRemaining,
-			float currentSpeed) {
-		this.mOverallTotal = overallTotal;
-		this.mOverallProgress = overallProgress;
-		this.mTimeRemaining = timeRemaining;
-		this.mCurrentSpeed = currentSpeed;
-	}
+    public static final Creator<DownloadProgressInfo> CREATOR = new Creator<DownloadProgressInfo>() {
+        @Override
+        public DownloadProgressInfo createFromParcel(Parcel parcel) {
+            return new DownloadProgressInfo(parcel);
+        }
 
-	public static final Creator<DownloadProgressInfo> CREATOR = new Creator<DownloadProgressInfo>() {
-		@Override
-		public DownloadProgressInfo createFromParcel(Parcel parcel) {
-			return new DownloadProgressInfo(parcel);
-		}
+        @Override
+        public DownloadProgressInfo[] newArray(int i) {
+            return new DownloadProgressInfo[i];
+        }
+    };
 
-		@Override
-		public DownloadProgressInfo[] newArray(int i) {
-			return new DownloadProgressInfo[i];
-		}
-	};
 }

+ 159 - 152
platform/android/java/src/com/google/android/vending/expansion/downloader/DownloaderClientMarshaller.java

@@ -32,7 +32,10 @@ import android.os.Messenger;
 import android.os.RemoteException;
 import android.util.Log;
 
+// -- GODOT start --
 import java.lang.ref.WeakReference;
+// -- GODOT end --
+
 
 /**
  * This class binds the service API to your application client.  It contains the IDownloaderClient proxy,
@@ -58,172 +61,175 @@ import java.lang.ref.WeakReference;
  * interface.
  */
 public class DownloaderClientMarshaller {
-	public static final int MSG_ONDOWNLOADSTATE_CHANGED = 10;
-	public static final int MSG_ONDOWNLOADPROGRESS = 11;
-	public static final int MSG_ONSERVICECONNECTED = 12;
+    public static final int MSG_ONDOWNLOADSTATE_CHANGED = 10;
+    public static final int MSG_ONDOWNLOADPROGRESS = 11;
+    public static final int MSG_ONSERVICECONNECTED = 12;
 
-	public static final String PARAM_NEW_STATE = "newState";
-	public static final String PARAM_PROGRESS = "progress";
-	public static final String PARAM_MESSENGER = DownloaderService.EXTRA_MESSAGE_HANDLER;
+    public static final String PARAM_NEW_STATE = "newState";
+    public static final String PARAM_PROGRESS = "progress";
+    public static final String PARAM_MESSENGER = DownloaderService.EXTRA_MESSAGE_HANDLER;
 
-	public static final int NO_DOWNLOAD_REQUIRED = DownloaderService.NO_DOWNLOAD_REQUIRED;
-	public static final int LVL_CHECK_REQUIRED = DownloaderService.LVL_CHECK_REQUIRED;
-	public static final int DOWNLOAD_REQUIRED = DownloaderService.DOWNLOAD_REQUIRED;
+    public static final int NO_DOWNLOAD_REQUIRED = DownloaderService.NO_DOWNLOAD_REQUIRED;
+    public static final int LVL_CHECK_REQUIRED = DownloaderService.LVL_CHECK_REQUIRED;
+    public static final int DOWNLOAD_REQUIRED = DownloaderService.DOWNLOAD_REQUIRED;
 
-	private static class Proxy implements IDownloaderClient {
-		private Messenger mServiceMessenger;
+    private static class Proxy implements IDownloaderClient {
+        private Messenger mServiceMessenger;
 
-		@Override
-		public void onDownloadStateChanged(int newState) {
-			Bundle params = new Bundle(1);
-			params.putInt(PARAM_NEW_STATE, newState);
-			send(MSG_ONDOWNLOADSTATE_CHANGED, params);
-		}
+        @Override
+        public void onDownloadStateChanged(int newState) {
+            Bundle params = new Bundle(1);
+            params.putInt(PARAM_NEW_STATE, newState);
+            send(MSG_ONDOWNLOADSTATE_CHANGED, params);
+        }
 
-		@Override
-		public void onDownloadProgress(DownloadProgressInfo progress) {
-			Bundle params = new Bundle(1);
-			params.putParcelable(PARAM_PROGRESS, progress);
-			send(MSG_ONDOWNLOADPROGRESS, params);
-		}
+        @Override
+        public void onDownloadProgress(DownloadProgressInfo progress) {
+            Bundle params = new Bundle(1);
+            params.putParcelable(PARAM_PROGRESS, progress);
+            send(MSG_ONDOWNLOADPROGRESS, params);
+        }
 
-		private void send(int method, Bundle params) {
-			Message m = Message.obtain(null, method);
-			m.setData(params);
-			try {
-				mServiceMessenger.send(m);
-			} catch (RemoteException e) {
-				e.printStackTrace();
-			}
-		}
+        private void send(int method, Bundle params) {
+            Message m = Message.obtain(null, method);
+            m.setData(params);
+            try {
+                mServiceMessenger.send(m);
+            } catch (RemoteException e) {
+                e.printStackTrace();
+            }
+        }
 
-		public Proxy(Messenger msg) {
-			mServiceMessenger = msg;
-		}
+        public Proxy(Messenger msg) {
+            mServiceMessenger = msg;
+        }
 
-		@Override
-		public void onServiceConnected(Messenger m) {
-			/**
+        @Override
+        public void onServiceConnected(Messenger m) {
+            /**
              * This is never called through the proxy.
              */
-		}
-	}
+        }
+    }
 
-	private static class Stub implements IStub {
-		private IDownloaderClient mItf = null;
-		private Class<?> mDownloaderServiceClass;
-		private boolean mBound;
-		private Messenger mServiceMessenger;
-		private Context mContext;
-		/**
+    private static class Stub implements IStub {
+        private IDownloaderClient mItf = null;
+        private Class<?> mDownloaderServiceClass;
+        private boolean mBound;
+        private Messenger mServiceMessenger;
+        private Context mContext;
+        /**
          * Target we publish for clients to send messages to IncomingHandler.
          */
-		private final MessengerHandlerClient mMsgHandler = new MessengerHandlerClient(this);
-		final Messenger mMessenger = new Messenger(mMsgHandler);
+        // -- GODOT start --
+        private final MessengerHandlerClient mMsgHandler = new MessengerHandlerClient(this);
+        final Messenger mMessenger = new Messenger(mMsgHandler);
 
-		private static class MessengerHandlerClient extends Handler {
-			private final WeakReference<Stub> mDownloader;
-			public MessengerHandlerClient(Stub downloader) {
-				mDownloader = new WeakReference<>(downloader);
-			}
+        private static class MessengerHandlerClient extends Handler {
+            private final WeakReference<Stub> mDownloader;
+            public MessengerHandlerClient(Stub downloader) {
+                mDownloader = new WeakReference<>(downloader);
+            }
 
-			@Override
-			public void handleMessage(Message msg) {
-				Stub downloader = mDownloader.get();
-				if (downloader != null) {
-					downloader.handleMessage(msg);
-				}
-			}
-		}
+            @Override
+            public void handleMessage(Message msg) {
+                Stub downloader = mDownloader.get();
+                if (downloader != null) {
+                    downloader.handleMessage(msg);
+                }
+            }
+        }
 
-		private void handleMessage(Message msg) {
-			switch (msg.what) {
-				case MSG_ONDOWNLOADPROGRESS:
-					Bundle bun = msg.getData();
-					if (null != mContext) {
-						bun.setClassLoader(mContext.getClassLoader());
-						DownloadProgressInfo dpi = (DownloadProgressInfo)msg.getData()
-														   .getParcelable(PARAM_PROGRESS);
-						mItf.onDownloadProgress(dpi);
-					}
-					break;
-				case MSG_ONDOWNLOADSTATE_CHANGED:
-					mItf.onDownloadStateChanged(msg.getData().getInt(PARAM_NEW_STATE));
-					break;
-				case MSG_ONSERVICECONNECTED:
-					mItf.onServiceConnected(
-							(Messenger)msg.getData().getParcelable(PARAM_MESSENGER));
-					break;
-			}
-		}
+        private void handleMessage(Message msg) {
+            switch (msg.what) {
+                case MSG_ONDOWNLOADPROGRESS:
+                    Bundle bun = msg.getData();
+                    if (null != mContext) {
+                        bun.setClassLoader(mContext.getClassLoader());
+                        DownloadProgressInfo dpi = (DownloadProgressInfo)msg.getData()
+                                                            .getParcelable(PARAM_PROGRESS);
+                        mItf.onDownloadProgress(dpi);
+                    }
+                    break;
+                case MSG_ONDOWNLOADSTATE_CHANGED:
+                    mItf.onDownloadStateChanged(msg.getData().getInt(PARAM_NEW_STATE));
+                    break;
+                case MSG_ONSERVICECONNECTED:
+                    mItf.onServiceConnected(
+                            (Messenger)msg.getData().getParcelable(PARAM_MESSENGER));
+                    break;
+            }
+        }
+        // -- GODOT end --
 
-		public Stub(IDownloaderClient itf, Class<?> downloaderService) {
-			mItf = itf;
-			mDownloaderServiceClass = downloaderService;
-		}
+        public Stub(IDownloaderClient itf, Class<?> downloaderService) {
+            mItf = itf;
+            mDownloaderServiceClass = downloaderService;
+        }
 
-		/**
+        /**
          * Class for interacting with the main interface of the service.
          */
-		private ServiceConnection mConnection = new ServiceConnection() {
-			public void onServiceConnected(ComponentName className, IBinder service) {
-				// This is called when the connection with the service has been
-				// established, giving us the object we can use to
-				// interact with the service. We are communicating with the
-				// service using a Messenger, so here we get a client-side
-				// representation of that from the raw IBinder object.
-				mServiceMessenger = new Messenger(service);
-				mItf.onServiceConnected(
-						mServiceMessenger);
-			}
+        private ServiceConnection mConnection = new ServiceConnection() {
+            public void onServiceConnected(ComponentName className, IBinder service) {
+                // This is called when the connection with the service has been
+                // established, giving us the object we can use to
+                // interact with the service. We are communicating with the
+                // service using a Messenger, so here we get a client-side
+                // representation of that from the raw IBinder object.
+                mServiceMessenger = new Messenger(service);
+                mItf.onServiceConnected(
+                        mServiceMessenger);
+            }
+
+            public void onServiceDisconnected(ComponentName className) {
+                // This is called when the connection with the service has been
+                // unexpectedly disconnected -- that is, its process crashed.
+                mServiceMessenger = null;
+            }
+        };
 
-			public void onServiceDisconnected(ComponentName className) {
-				// This is called when the connection with the service has been
-				// unexpectedly disconnected -- that is, its process crashed.
-				mServiceMessenger = null;
-			}
-		};
+        @Override
+        public void connect(Context c) {
+            mContext = c;
+            Intent bindIntent = new Intent(c, mDownloaderServiceClass);
+            bindIntent.putExtra(PARAM_MESSENGER, mMessenger);
+            if ( !c.bindService(bindIntent, mConnection, Context.BIND_DEBUG_UNBIND) ) {
+                if ( Constants.LOGVV ) {
+                    Log.d(Constants.TAG, "Service Unbound");
+                }
+            } else {
+                mBound = true;
+            }
 
-		@Override
-		public void connect(Context c) {
-			mContext = c;
-			Intent bindIntent = new Intent(c, mDownloaderServiceClass);
-			bindIntent.putExtra(PARAM_MESSENGER, mMessenger);
-			if (!c.bindService(bindIntent, mConnection, Context.BIND_DEBUG_UNBIND)) {
-				if (Constants.LOGVV) {
-					Log.d(Constants.TAG, "Service Unbound");
-				}
-			} else {
-				mBound = true;
-			}
-		}
+        }
 
-		@Override
-		public void disconnect(Context c) {
-			if (mBound) {
-				c.unbindService(mConnection);
-				mBound = false;
-			}
-			mContext = null;
-		}
+        @Override
+        public void disconnect(Context c) {
+            if (mBound) {
+                c.unbindService(mConnection);
+                mBound = false;
+            }
+            mContext = null;
+        }
 
-		@Override
-		public Messenger getMessenger() {
-			return mMessenger;
-		}
-	}
+        @Override
+        public Messenger getMessenger() {
+            return mMessenger;
+        }
+    }
 
-	/**
+    /**
      * Returns a proxy that will marshal calls to IDownloaderClient methods
      *
      * @param msg
      * @return
      */
-	public static IDownloaderClient CreateProxy(Messenger msg) {
-		return new Proxy(msg);
-	}
+    public static IDownloaderClient CreateProxy(Messenger msg) {
+        return new Proxy(msg);
+    }
 
-	/**
+    /**
      * Returns a stub object that, when connected, will listen for marshaled
      * {@link IDownloaderClient} methods and translate them into calls to the supplied
      * interface.
@@ -235,11 +241,11 @@ public class DownloaderClientMarshaller {
      * @return The {@link IStub} that allows you to connect to the service such that
      * your {@link IDownloaderClient} receives status updates.
      */
-	public static IStub CreateStub(IDownloaderClient itf, Class<?> downloaderService) {
-		return new Stub(itf, downloaderService);
-	}
+    public static IStub CreateStub(IDownloaderClient itf, Class<?> downloaderService) {
+        return new Stub(itf, downloaderService);
+    }
 
-	/**
+    /**
      * Starts the download if necessary. This function starts a flow that does `
      * many things. 1) Checks to see if the APK version has been checked and
      * the metadata database updated 2) If the APK version does not match,
@@ -262,14 +268,14 @@ public class DownloaderClientMarshaller {
      * #DOWNLOAD_REQUIRED}.
      * @throws NameNotFoundException
      */
-	public static int startDownloadServiceIfRequired(Context context, PendingIntent notificationClient,
-			Class<?> serviceClass)
-			throws NameNotFoundException {
-		return DownloaderService.startDownloadServiceIfRequired(context, notificationClient,
-				serviceClass);
-	}
+    public static int startDownloadServiceIfRequired(Context context, PendingIntent notificationClient,
+            Class<?> serviceClass)
+            throws NameNotFoundException {
+        return DownloaderService.startDownloadServiceIfRequired(context, notificationClient,
+                serviceClass);
+    }
 
-	/**
+    /**
      * This version assumes that the intent contains the pending intent as a parameter. This
      * is used for responding to alarms.
      * <p>The pending intent must be in an extra with the key {@link
@@ -281,10 +287,11 @@ public class DownloaderClientMarshaller {
      * @return
      * @throws NameNotFoundException
      */
-	public static int startDownloadServiceIfRequired(Context context, Intent notificationClient,
-			Class<?> serviceClass)
-			throws NameNotFoundException {
-		return DownloaderService.startDownloadServiceIfRequired(context, notificationClient,
-				serviceClass);
-	}
+    public static int startDownloadServiceIfRequired(Context context, Intent notificationClient,
+            Class<?> serviceClass)
+            throws NameNotFoundException {
+        return DownloaderService.startDownloadServiceIfRequired(context, notificationClient,
+                serviceClass);
+    }
+
 }

+ 146 - 138
platform/android/java/src/com/google/android/vending/expansion/downloader/DownloaderServiceMarshaller.java

@@ -25,7 +25,10 @@ import android.os.Message;
 import android.os.Messenger;
 import android.os.RemoteException;
 
+// -- GODOT start --
 import java.lang.ref.WeakReference;
+// -- GODOT end --
+
 
 /**
  * This class is used by the client activity to proxy requests to the Downloader
@@ -38,147 +41,151 @@ import java.lang.ref.WeakReference;
  */
 public class DownloaderServiceMarshaller {
 
-	public static final int MSG_REQUEST_ABORT_DOWNLOAD =
-			1;
-	public static final int MSG_REQUEST_PAUSE_DOWNLOAD =
-			2;
-	public static final int MSG_SET_DOWNLOAD_FLAGS =
-			3;
-	public static final int MSG_REQUEST_CONTINUE_DOWNLOAD =
-			4;
-	public static final int MSG_REQUEST_DOWNLOAD_STATE =
-			5;
-	public static final int MSG_REQUEST_CLIENT_UPDATE =
-			6;
-
-	public static final String PARAMS_FLAGS = "flags";
-	public static final String PARAM_MESSENGER = DownloaderService.EXTRA_MESSAGE_HANDLER;
-
-	private static class Proxy implements IDownloaderService {
-		private Messenger mMsg;
-
-		private void send(int method, Bundle params) {
-			Message m = Message.obtain(null, method);
-			m.setData(params);
-			try {
-				mMsg.send(m);
-			} catch (RemoteException e) {
-				e.printStackTrace();
-			}
-		}
-
-		public Proxy(Messenger msg) {
-			mMsg = msg;
-		}
-
-		@Override
-		public void requestAbortDownload() {
-			send(MSG_REQUEST_ABORT_DOWNLOAD, new Bundle());
-		}
-
-		@Override
-		public void requestPauseDownload() {
-			send(MSG_REQUEST_PAUSE_DOWNLOAD, new Bundle());
-		}
-
-		@Override
-		public void setDownloadFlags(int flags) {
-			Bundle params = new Bundle();
-			params.putInt(PARAMS_FLAGS, flags);
-			send(MSG_SET_DOWNLOAD_FLAGS, params);
-		}
-
-		@Override
-		public void requestContinueDownload() {
-			send(MSG_REQUEST_CONTINUE_DOWNLOAD, new Bundle());
-		}
-
-		@Override
-		public void requestDownloadStatus() {
-			send(MSG_REQUEST_DOWNLOAD_STATE, new Bundle());
-		}
-
-		@Override
-		public void onClientUpdated(Messenger clientMessenger) {
-			Bundle bundle = new Bundle(1);
-			bundle.putParcelable(PARAM_MESSENGER, clientMessenger);
-			send(MSG_REQUEST_CLIENT_UPDATE, bundle);
-		}
-	}
-
-	private static class Stub implements IStub {
-		private IDownloaderService mItf = null;
-		private final MessengerHandlerServer mMsgHandler = new MessengerHandlerServer(this);
-		final Messenger mMessenger = new Messenger(mMsgHandler);
-
-		private static class MessengerHandlerServer extends Handler {
-			private final WeakReference<Stub> mDownloader;
-			public MessengerHandlerServer(Stub downloader) {
-				mDownloader = new WeakReference<>(downloader);
-			}
-
-			@Override
-			public void handleMessage(Message msg) {
-				Stub downloader = mDownloader.get();
-				if (downloader != null) {
-					downloader.handleMessage(msg);
-				}
-			}
-		}
-
-		private void handleMessage(Message msg) {
-			switch (msg.what) {
-				case MSG_REQUEST_ABORT_DOWNLOAD:
-					mItf.requestAbortDownload();
-					break;
-				case MSG_REQUEST_CONTINUE_DOWNLOAD:
-					mItf.requestContinueDownload();
-					break;
-				case MSG_REQUEST_PAUSE_DOWNLOAD:
-					mItf.requestPauseDownload();
-					break;
-				case MSG_SET_DOWNLOAD_FLAGS:
-					mItf.setDownloadFlags(msg.getData().getInt(PARAMS_FLAGS));
-					break;
-				case MSG_REQUEST_DOWNLOAD_STATE:
-					mItf.requestDownloadStatus();
-					break;
-				case MSG_REQUEST_CLIENT_UPDATE:
-					mItf.onClientUpdated((Messenger)msg.getData().getParcelable(
-							PARAM_MESSENGER));
-					break;
-			}
-		}
-
-		public Stub(IDownloaderService itf) {
-			mItf = itf;
-		}
-
-		@Override
-		public Messenger getMessenger() {
-			return mMessenger;
-		}
-
-		@Override
-		public void connect(Context c) {
-		}
-
-		@Override
-		public void disconnect(Context c) {
-		}
-	}
-
-	/**
+    public static final int MSG_REQUEST_ABORT_DOWNLOAD =
+            1;
+    public static final int MSG_REQUEST_PAUSE_DOWNLOAD =
+            2;
+    public static final int MSG_SET_DOWNLOAD_FLAGS =
+            3;
+    public static final int MSG_REQUEST_CONTINUE_DOWNLOAD =
+            4;
+    public static final int MSG_REQUEST_DOWNLOAD_STATE =
+            5;
+    public static final int MSG_REQUEST_CLIENT_UPDATE =
+            6;
+
+    public static final String PARAMS_FLAGS = "flags";
+    public static final String PARAM_MESSENGER = DownloaderService.EXTRA_MESSAGE_HANDLER;
+
+    private static class Proxy implements IDownloaderService {
+        private Messenger mMsg;
+
+        private void send(int method, Bundle params) {
+            Message m = Message.obtain(null, method);
+            m.setData(params);
+            try {
+                mMsg.send(m);
+            } catch (RemoteException e) {
+                e.printStackTrace();
+            }
+        }
+
+        public Proxy(Messenger msg) {
+            mMsg = msg;
+        }
+
+        @Override
+        public void requestAbortDownload() {
+            send(MSG_REQUEST_ABORT_DOWNLOAD, new Bundle());
+        }
+
+        @Override
+        public void requestPauseDownload() {
+            send(MSG_REQUEST_PAUSE_DOWNLOAD, new Bundle());
+        }
+
+        @Override
+        public void setDownloadFlags(int flags) {
+            Bundle params = new Bundle();
+            params.putInt(PARAMS_FLAGS, flags);
+            send(MSG_SET_DOWNLOAD_FLAGS, params);
+        }
+
+        @Override
+        public void requestContinueDownload() {
+            send(MSG_REQUEST_CONTINUE_DOWNLOAD, new Bundle());
+        }
+
+        @Override
+        public void requestDownloadStatus() {
+            send(MSG_REQUEST_DOWNLOAD_STATE, new Bundle());
+        }
+
+        @Override
+        public void onClientUpdated(Messenger clientMessenger) {
+            Bundle bundle = new Bundle(1);
+            bundle.putParcelable(PARAM_MESSENGER, clientMessenger);
+            send(MSG_REQUEST_CLIENT_UPDATE, bundle);
+        }
+    }
+
+    private static class Stub implements IStub {
+        private IDownloaderService mItf = null;
+        // -- GODOT start --
+        private final MessengerHandlerServer mMsgHandler = new MessengerHandlerServer(this);
+        final Messenger mMessenger = new Messenger(mMsgHandler);
+
+        private static class MessengerHandlerServer extends Handler {
+            private final WeakReference<Stub> mDownloader;
+            public MessengerHandlerServer(Stub downloader) {
+                mDownloader = new WeakReference<>(downloader);
+            }
+
+            @Override
+            public void handleMessage(Message msg) {
+                Stub downloader = mDownloader.get();
+                if (downloader != null) {
+                    downloader.handleMessage(msg);
+                }
+            }
+        }
+
+        private void handleMessage(Message msg) {
+            switch (msg.what) {
+                case MSG_REQUEST_ABORT_DOWNLOAD:
+                    mItf.requestAbortDownload();
+                    break;
+                case MSG_REQUEST_CONTINUE_DOWNLOAD:
+                    mItf.requestContinueDownload();
+                    break;
+                case MSG_REQUEST_PAUSE_DOWNLOAD:
+                    mItf.requestPauseDownload();
+                    break;
+                case MSG_SET_DOWNLOAD_FLAGS:
+                    mItf.setDownloadFlags(msg.getData().getInt(PARAMS_FLAGS));
+                    break;
+                case MSG_REQUEST_DOWNLOAD_STATE:
+                    mItf.requestDownloadStatus();
+                    break;
+                case MSG_REQUEST_CLIENT_UPDATE:
+                    mItf.onClientUpdated((Messenger)msg.getData().getParcelable(
+                            PARAM_MESSENGER));
+                    break;
+            }
+        }
+        // -- GODOT end --
+
+        public Stub(IDownloaderService itf) {
+            mItf = itf;
+        }
+
+        @Override
+        public Messenger getMessenger() {
+            return mMessenger;
+        }
+
+        @Override
+        public void connect(Context c) {
+
+        }
+
+        @Override
+        public void disconnect(Context c) {
+
+        }
+    }
+
+    /**
      * Returns a proxy that will marshall calls to IDownloaderService methods
      *
      * @param ctx
      * @return
      */
-	public static IDownloaderService CreateProxy(Messenger msg) {
-		return new Proxy(msg);
-	}
+    public static IDownloaderService CreateProxy(Messenger msg) {
+        return new Proxy(msg);
+    }
 
-	/**
+    /**
      * Returns a stub object that, when connected, will listen for marshalled
      * IDownloaderService methods and translate them into calls to the supplied
      * interface.
@@ -187,7 +194,8 @@ public class DownloaderServiceMarshaller {
      *            when remote method calls are unmarshalled.
      * @return
      */
-	public static IStub CreateStub(IDownloaderService itf) {
-		return new Stub(itf);
-	}
+    public static IStub CreateStub(IDownloaderService itf) {
+        return new Stub(itf);
+    }
+
 }

+ 239 - 227
platform/android/java/src/com/google/android/vending/expansion/downloader/Helpers.java

@@ -24,7 +24,10 @@ import android.os.StatFs;
 import android.os.SystemClock;
 import android.util.Log;
 
+// -- GODOT start --
+//import com.android.vending.expansion.downloader.R;
 import com.godot.game.R;
+// -- GODOT end --
 
 import java.io.File;
 import java.text.SimpleDateFormat;
@@ -40,95 +43,96 @@ import java.util.regex.Pattern;
  */
 public class Helpers {
 
-	public static Random sRandom = new Random(SystemClock.uptimeMillis());
+    public static Random sRandom = new Random(SystemClock.uptimeMillis());
 
-	/** Regex used to parse content-disposition headers */
-	private static final Pattern CONTENT_DISPOSITION_PATTERN = Pattern
-																	   .compile("attachment;\\s*filename\\s*=\\s*\"([^\"]*)\"");
+    /** Regex used to parse content-disposition headers */
+    private static final Pattern CONTENT_DISPOSITION_PATTERN = Pattern
+            .compile("attachment;\\s*filename\\s*=\\s*\"([^\"]*)\"");
 
-	private Helpers() {
-	}
+    private Helpers() {
+    }
 
-	/*
+    /*
      * Parse the Content-Disposition HTTP Header. The format of the header is defined here:
      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html This header provides a filename for
      * content that is going to be downloaded to the file system. We only support the attachment
      * type.
      */
-	static String parseContentDisposition(String contentDisposition) {
-		try {
-			Matcher m = CONTENT_DISPOSITION_PATTERN.matcher(contentDisposition);
-			if (m.find()) {
-				return m.group(1);
-			}
-		} catch (IllegalStateException ex) {
-			// This function is defined as returning null when it can't parse
-			// the header
-		}
-		return null;
-	}
+    static String parseContentDisposition(String contentDisposition) {
+        try {
+            Matcher m = CONTENT_DISPOSITION_PATTERN.matcher(contentDisposition);
+            if (m.find()) {
+                return m.group(1);
+            }
+        } catch (IllegalStateException ex) {
+            // This function is defined as returning null when it can't parse
+            // the header
+        }
+        return null;
+    }
 
-	/**
+    /**
      * @return the root of the filesystem containing the given path
      */
-	public static File getFilesystemRoot(String path) {
-		File cache = Environment.getDownloadCacheDirectory();
-		if (path.startsWith(cache.getPath())) {
-			return cache;
-		}
-		File external = Environment.getExternalStorageDirectory();
-		if (path.startsWith(external.getPath())) {
-			return external;
-		}
-		throw new IllegalArgumentException(
-				"Cannot determine filesystem root for " + path);
-	}
+    public static File getFilesystemRoot(String path) {
+        File cache = Environment.getDownloadCacheDirectory();
+        if (path.startsWith(cache.getPath())) {
+            return cache;
+        }
+        File external = Environment.getExternalStorageDirectory();
+        if (path.startsWith(external.getPath())) {
+            return external;
+        }
+        throw new IllegalArgumentException(
+                "Cannot determine filesystem root for " + path);
+    }
 
-	public static boolean isExternalMediaMounted() {
-		if (!Environment.getExternalStorageState().equals(
-					Environment.MEDIA_MOUNTED)) {
-			// No SD card found.
-			if (Constants.LOGVV) {
-				Log.d(Constants.TAG, "no external storage");
-			}
-			return false;
-		}
-		return true;
-	}
+    public static boolean isExternalMediaMounted() {
+        if (!Environment.getExternalStorageState().equals(
+                Environment.MEDIA_MOUNTED)) {
+            // No SD card found.
+            if (Constants.LOGVV) {
+                Log.d(Constants.TAG, "no external storage");
+            }
+            return false;
+        }
+        return true;
+    }
 
-	/**
+    /**
      * @return the number of bytes available on the filesystem rooted at the given File
      */
-	public static long getAvailableBytes(File root) {
-		StatFs stat = new StatFs(root.getPath());
-		// put a bit of margin (in case creating the file grows the system by a
-		// few blocks)
-		long availableBlocks = (long)stat.getAvailableBlocks() - 4;
-		return stat.getBlockSize() * availableBlocks;
-	}
+    public static long getAvailableBytes(File root) {
+        StatFs stat = new StatFs(root.getPath());
+        // put a bit of margin (in case creating the file grows the system by a
+        // few blocks)
+        long availableBlocks = (long) stat.getAvailableBlocks() - 4;
+        return stat.getBlockSize() * availableBlocks;
+    }
 
-	/**
+    /**
      * Checks whether the filename looks legitimate
      */
-	public static boolean isFilenameValid(String filename) {
-		filename = filename.replaceFirst("/+", "/"); // normalize leading
-				// slashes
-		return filename.startsWith(Environment.getDownloadCacheDirectory().toString()) || filename.startsWith(Environment.getExternalStorageDirectory().toString());
-	}
+    public static boolean isFilenameValid(String filename) {
+        filename = filename.replaceFirst("/+", "/"); // normalize leading
+                                                     // slashes
+        return filename.startsWith(Environment.getDownloadCacheDirectory().toString())
+                || filename.startsWith(Environment.getExternalStorageDirectory().toString());
+    }
 
-	/*
+    /*
      * Delete the given file from device
      */
-	/* package */ static void deleteFile(String path) {
-		try {
-			File file = new File(path);
-			file.delete();
-		} catch (Exception e) {
-			Log.w(Constants.TAG, "file: '" + path + "' couldn't be deleted", e);
-		}
-	}
+    /* package */static void deleteFile(String path) {
+        try {
+            File file = new File(path);
+            file.delete();
+        } catch (Exception e) {
+            Log.w(Constants.TAG, "file: '" + path + "' couldn't be deleted", e);
+        }
+    }
 
-	/**
+    /**
      * Showing progress in MB here. It would be nice to choose the unit (KB, MB, GB) based on total
      * file size, but given what we know about the expected ranges of file sizes for APK expansion
      * files, it's probably not necessary.
@@ -138,63 +142,69 @@ public class Helpers {
      * @return
      */
 
-	static public String getDownloadProgressString(long overallProgress, long overallTotal) {
-		if (overallTotal == 0) {
-			if (Constants.LOGVV) {
-				Log.e(Constants.TAG, "Notification called when total is zero");
-			}
-			return "";
-		}
-		return String.format(Locale.ENGLISH, "%.2f",
-					   (float)overallProgress / (1024.0f * 1024.0f)) +
-				"MB /" +
-				String.format(Locale.ENGLISH, "%.2f", (float)overallTotal / (1024.0f * 1024.0f)) + "MB";
-	}
+    static public String getDownloadProgressString(long overallProgress, long overallTotal) {
+        if (overallTotal == 0) {
+            if (Constants.LOGVV) {
+                Log.e(Constants.TAG, "Notification called when total is zero");
+            }
+            return "";
+        }
+        // -- GODOT start --
+        return String.format(Locale.ENGLISH, "%.2f",
+                (float) overallProgress / (1024.0f * 1024.0f))
+                + "MB /" +
+                String.format(Locale.ENGLISH, "%.2f", (float) overallTotal /
+                        (1024.0f * 1024.0f))
+                + "MB";
+        // -- GODOT end --
+    }
 
-	/**
+    /**
      * Adds a percentile to getDownloadProgressString.
      *
      * @param overallProgress
      * @param overallTotal
      * @return
      */
-	static public String getDownloadProgressStringNotification(long overallProgress,
-			long overallTotal) {
-		if (overallTotal == 0) {
-			if (Constants.LOGVV) {
-				Log.e(Constants.TAG, "Notification called when total is zero");
-			}
-			return "";
-		}
-		return getDownloadProgressString(overallProgress, overallTotal) + " (" +
-				getDownloadProgressPercent(overallProgress, overallTotal) + ")";
-	}
+    static public String getDownloadProgressStringNotification(long overallProgress,
+            long overallTotal) {
+        if (overallTotal == 0) {
+            if (Constants.LOGVV) {
+                Log.e(Constants.TAG, "Notification called when total is zero");
+            }
+            return "";
+        }
+        return getDownloadProgressString(overallProgress, overallTotal) + " (" +
+                getDownloadProgressPercent(overallProgress, overallTotal) + ")";
+    }
 
-	public static String getDownloadProgressPercent(long overallProgress, long overallTotal) {
-		if (overallTotal == 0) {
-			if (Constants.LOGVV) {
-				Log.e(Constants.TAG, "Notification called when total is zero");
-			}
-			return "";
-		}
-		return Long.toString(overallProgress * 100 / overallTotal) + "%";
-	}
+    public static String getDownloadProgressPercent(long overallProgress, long overallTotal) {
+        if (overallTotal == 0) {
+            if (Constants.LOGVV) {
+                Log.e(Constants.TAG, "Notification called when total is zero");
+            }
+            return "";
+        }
+        return Long.toString(overallProgress * 100 / overallTotal) + "%";
+    }
 
-	public static String getSpeedString(float bytesPerMillisecond) {
-		return String.format(Locale.ENGLISH, "%.2f", bytesPerMillisecond * 1000 / 1024);
-	}
+    public static String getSpeedString(float bytesPerMillisecond) {
+        // -- GODOT start --
+        return String.format(Locale.ENGLISH, "%.2f", bytesPerMillisecond * 1000 / 1024);
+        // -- GODOT end --
+    }
 
-	public static String getTimeRemaining(long durationInMilliseconds) {
-		SimpleDateFormat sdf;
-		if (durationInMilliseconds > 1000 * 60 * 60) {
-			sdf = new SimpleDateFormat("HH:mm", Locale.getDefault());
-		} else {
-			sdf = new SimpleDateFormat("mm:ss", Locale.getDefault());
-		}
-		return sdf.format(new Date(durationInMilliseconds - TimeZone.getDefault().getRawOffset()));
-	}
+    public static String getTimeRemaining(long durationInMilliseconds) {
+        SimpleDateFormat sdf;
+        if (durationInMilliseconds > 1000 * 60 * 60) {
+            sdf = new SimpleDateFormat("HH:mm", Locale.getDefault());
+        } else {
+            sdf = new SimpleDateFormat("mm:ss", Locale.getDefault());
+        }
+        return sdf.format(new Date(durationInMilliseconds - TimeZone.getDefault().getRawOffset()));
+    }
 
-	/**
+    /**
      * Returns the file name (without full path) for an Expansion APK file from the given context.
      *
      * @param c the context
@@ -202,33 +212,34 @@ public class Helpers {
      * @param versionCode the version of the file
      * @return String the file name of the expansion file
      */
-	public static String getExpansionAPKFileName(Context c, boolean mainFile, int versionCode) {
-		return (mainFile ? "main." : "patch.") + versionCode + "." + c.getPackageName() + ".obb";
-	}
+    public static String getExpansionAPKFileName(Context c, boolean mainFile, int versionCode) {
+        return (mainFile ? "main." : "patch.") + versionCode + "." + c.getPackageName() + ".obb";
+    }
 
-	/**
+    /**
      * Returns the filename (where the file should be saved) from info about a download
      */
-	static public String generateSaveFileName(Context c, String fileName) {
-		String path = getSaveFilePath(c) + File.separator + fileName;
-		return path;
-	}
+    static public String generateSaveFileName(Context c, String fileName) {
+        String path = getSaveFilePath(c)
+                + File.separator + fileName;
+        return path;
+    }
 
-	@TargetApi(Build.VERSION_CODES.HONEYCOMB)
-	static public String getSaveFilePath(Context c) {
-		// This technically existed since Honeycomb, but it is critical
-		// on KitKat and greater versions since it will create the
-		// directory if needed
-		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
-			return c.getObbDir().toString();
-		} else {
-			File root = Environment.getExternalStorageDirectory();
-			String path = root.toString() + Constants.EXP_PATH + c.getPackageName();
-			return path;
-		}
-	}
+    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+    static public String getSaveFilePath(Context c) {
+        // This technically existed since Honeycomb, but it is critical
+        // on KitKat and greater versions since it will create the
+        // directory if needed
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+            return c.getObbDir().toString();
+        } else {
+            File root = Environment.getExternalStorageDirectory();
+            String path = root.toString() + Constants.EXP_PATH + c.getPackageName();
+            return path;
+        }
+    }
 
-	/**
+    /**
      * Helper function to ascertain the existence of a file and return true/false appropriately
      *
      * @param c the app/activity/service context
@@ -237,72 +248,72 @@ public class Helpers {
      * @param deleteFileOnMismatch if the file sizes do not match, delete the file
      * @return true if it does exist, false otherwise
      */
-	static public boolean doesFileExist(Context c, String fileName, long fileSize,
-			boolean deleteFileOnMismatch) {
-		// the file may have been delivered by Play --- let's make sure
-		// it's the size we expect
-		File fileForNewFile = new File(Helpers.generateSaveFileName(c, fileName));
-		if (fileForNewFile.exists()) {
-			if (fileForNewFile.length() == fileSize) {
-				return true;
-			}
-			if (deleteFileOnMismatch) {
-				// delete the file --- we won't be able to resume
-				// because we cannot confirm the integrity of the file
-				fileForNewFile.delete();
-			}
-		}
-		return false;
-	}
+    static public boolean doesFileExist(Context c, String fileName, long fileSize,
+            boolean deleteFileOnMismatch) {
+        // the file may have been delivered by Play --- let's make sure
+        // it's the size we expect
+        File fileForNewFile = new File(Helpers.generateSaveFileName(c, fileName));
+        if (fileForNewFile.exists()) {
+            if (fileForNewFile.length() == fileSize) {
+                return true;
+            }
+            if (deleteFileOnMismatch) {
+                // delete the file --- we won't be able to resume
+                // because we cannot confirm the integrity of the file
+                fileForNewFile.delete();
+            }
+        }
+        return false;
+    }
 
-	public static final int FS_READABLE = 0;
-	public static final int FS_DOES_NOT_EXIST = 1;
-	public static final int FS_CANNOT_READ = 2;
+    public static final int FS_READABLE = 0;
+    public static final int FS_DOES_NOT_EXIST = 1;
+    public static final int FS_CANNOT_READ = 2;
 
-	/**
+    /**
      * Helper function to ascertain whether a file can be read.
      *
      * @param c the app/activity/service context
      * @param fileName the name (sans path) of the file to query
      * @return true if it does exist, false otherwise
      */
-	static public int getFileStatus(Context c, String fileName) {
-		// the file may have been delivered by Play --- let's make sure
-		// it's the size we expect
-		File fileForNewFile = new File(Helpers.generateSaveFileName(c, fileName));
-		int returnValue;
-		if (fileForNewFile.exists()) {
-			if (fileForNewFile.canRead()) {
-				returnValue = FS_READABLE;
-			} else {
-				returnValue = FS_CANNOT_READ;
-			}
-		} else {
-			returnValue = FS_DOES_NOT_EXIST;
-		}
-		return returnValue;
-	}
+    static public int getFileStatus(Context c, String fileName) {
+        // the file may have been delivered by Play --- let's make sure
+        // it's the size we expect
+        File fileForNewFile = new File(Helpers.generateSaveFileName(c, fileName));
+        int returnValue;
+        if (fileForNewFile.exists()) {
+            if (fileForNewFile.canRead()) {
+                returnValue = FS_READABLE;
+            } else {
+                returnValue = FS_CANNOT_READ;
+            }
+        } else {
+            returnValue = FS_DOES_NOT_EXIST;
+        }
+        return returnValue;
+    }
 
-	/**
+    /**
      * Helper function to ascertain whether the application has the correct access to the OBB
      * directory to allow an OBB file to be written.
      * 
      * @param c the app/activity/service context
      * @return true if the application can write an OBB file, false otherwise
      */
-	static public boolean canWriteOBBFile(Context c) {
-		String path = getSaveFilePath(c);
-		File fileForNewFile = new File(path);
-		boolean canWrite;
-		if (fileForNewFile.exists()) {
-			canWrite = fileForNewFile.isDirectory() && fileForNewFile.canWrite();
-		} else {
-			canWrite = fileForNewFile.mkdirs();
-		}
-		return canWrite;
-	}
+    static public boolean canWriteOBBFile(Context c) {
+        String path = getSaveFilePath(c);
+        File fileForNewFile = new File(path);
+        boolean canWrite;
+        if (fileForNewFile.exists()) {
+            canWrite = fileForNewFile.isDirectory() && fileForNewFile.canWrite();
+        } else {
+            canWrite = fileForNewFile.mkdirs();
+        }
+        return canWrite;
+    }
 
-	/**
+    /**
      * Converts download states that are returned by the
      * {@link IDownloaderClient#onDownloadStateChanged} callback into usable strings. This is useful
      * if using the state strings built into the library to display user messages.
@@ -310,46 +321,47 @@ public class Helpers {
      * @param state One of the STATE_* constants from {@link IDownloaderClient}.
      * @return string resource ID for the corresponding string.
      */
-	static public int getDownloaderStringResourceIDFromState(int state) {
-		switch (state) {
-			case IDownloaderClient.STATE_IDLE:
-				return R.string.state_idle;
-			case IDownloaderClient.STATE_FETCHING_URL:
-				return R.string.state_fetching_url;
-			case IDownloaderClient.STATE_CONNECTING:
-				return R.string.state_connecting;
-			case IDownloaderClient.STATE_DOWNLOADING:
-				return R.string.state_downloading;
-			case IDownloaderClient.STATE_COMPLETED:
-				return R.string.state_completed;
-			case IDownloaderClient.STATE_PAUSED_NETWORK_UNAVAILABLE:
-				return R.string.state_paused_network_unavailable;
-			case IDownloaderClient.STATE_PAUSED_BY_REQUEST:
-				return R.string.state_paused_by_request;
-			case IDownloaderClient.STATE_PAUSED_WIFI_DISABLED_NEED_CELLULAR_PERMISSION:
-				return R.string.state_paused_wifi_disabled;
-			case IDownloaderClient.STATE_PAUSED_NEED_CELLULAR_PERMISSION:
-				return R.string.state_paused_wifi_unavailable;
-			case IDownloaderClient.STATE_PAUSED_WIFI_DISABLED:
-				return R.string.state_paused_wifi_disabled;
-			case IDownloaderClient.STATE_PAUSED_NEED_WIFI:
-				return R.string.state_paused_wifi_unavailable;
-			case IDownloaderClient.STATE_PAUSED_ROAMING:
-				return R.string.state_paused_roaming;
-			case IDownloaderClient.STATE_PAUSED_NETWORK_SETUP_FAILURE:
-				return R.string.state_paused_network_setup_failure;
-			case IDownloaderClient.STATE_PAUSED_SDCARD_UNAVAILABLE:
-				return R.string.state_paused_sdcard_unavailable;
-			case IDownloaderClient.STATE_FAILED_UNLICENSED:
-				return R.string.state_failed_unlicensed;
-			case IDownloaderClient.STATE_FAILED_FETCHING_URL:
-				return R.string.state_failed_fetching_url;
-			case IDownloaderClient.STATE_FAILED_SDCARD_FULL:
-				return R.string.state_failed_sdcard_full;
-			case IDownloaderClient.STATE_FAILED_CANCELED:
-				return R.string.state_failed_cancelled;
-			default:
-				return R.string.state_unknown;
-		}
-	}
+    static public int getDownloaderStringResourceIDFromState(int state) {
+        switch (state) {
+            case IDownloaderClient.STATE_IDLE:
+                return R.string.state_idle;
+            case IDownloaderClient.STATE_FETCHING_URL:
+                return R.string.state_fetching_url;
+            case IDownloaderClient.STATE_CONNECTING:
+                return R.string.state_connecting;
+            case IDownloaderClient.STATE_DOWNLOADING:
+                return R.string.state_downloading;
+            case IDownloaderClient.STATE_COMPLETED:
+                return R.string.state_completed;
+            case IDownloaderClient.STATE_PAUSED_NETWORK_UNAVAILABLE:
+                return R.string.state_paused_network_unavailable;
+            case IDownloaderClient.STATE_PAUSED_BY_REQUEST:
+                return R.string.state_paused_by_request;
+            case IDownloaderClient.STATE_PAUSED_WIFI_DISABLED_NEED_CELLULAR_PERMISSION:
+                return R.string.state_paused_wifi_disabled;
+            case IDownloaderClient.STATE_PAUSED_NEED_CELLULAR_PERMISSION:
+                return R.string.state_paused_wifi_unavailable;
+            case IDownloaderClient.STATE_PAUSED_WIFI_DISABLED:
+                return R.string.state_paused_wifi_disabled;
+            case IDownloaderClient.STATE_PAUSED_NEED_WIFI:
+                return R.string.state_paused_wifi_unavailable;
+            case IDownloaderClient.STATE_PAUSED_ROAMING:
+                return R.string.state_paused_roaming;
+            case IDownloaderClient.STATE_PAUSED_NETWORK_SETUP_FAILURE:
+                return R.string.state_paused_network_setup_failure;
+            case IDownloaderClient.STATE_PAUSED_SDCARD_UNAVAILABLE:
+                return R.string.state_paused_sdcard_unavailable;
+            case IDownloaderClient.STATE_FAILED_UNLICENSED:
+                return R.string.state_failed_unlicensed;
+            case IDownloaderClient.STATE_FAILED_FETCHING_URL:
+                return R.string.state_failed_fetching_url;
+            case IDownloaderClient.STATE_FAILED_SDCARD_FULL:
+                return R.string.state_failed_sdcard_full;
+            case IDownloaderClient.STATE_FAILED_CANCELED:
+                return R.string.state_failed_cancelled;
+            default:
+                return R.string.state_unknown;
+        }
+    }
+
 }

+ 28 - 28
platform/android/java/src/com/google/android/vending/expansion/downloader/IDownloaderClient.java

@@ -23,26 +23,26 @@ import android.os.Messenger;
  * downloader. It is used to pass status from the service to the client.
  */
 public interface IDownloaderClient {
-	static final int STATE_IDLE = 1;
-	static final int STATE_FETCHING_URL = 2;
-	static final int STATE_CONNECTING = 3;
-	static final int STATE_DOWNLOADING = 4;
-	static final int STATE_COMPLETED = 5;
+    static final int STATE_IDLE = 1;
+    static final int STATE_FETCHING_URL = 2;
+    static final int STATE_CONNECTING = 3;
+    static final int STATE_DOWNLOADING = 4;
+    static final int STATE_COMPLETED = 5;
 
-	static final int STATE_PAUSED_NETWORK_UNAVAILABLE = 6;
-	static final int STATE_PAUSED_BY_REQUEST = 7;
+    static final int STATE_PAUSED_NETWORK_UNAVAILABLE = 6;
+    static final int STATE_PAUSED_BY_REQUEST = 7;
 
-	/**
+    /**
      * Both STATE_PAUSED_WIFI_DISABLED_NEED_CELLULAR_PERMISSION and
      * STATE_PAUSED_NEED_CELLULAR_PERMISSION imply that Wi-Fi is unavailable and
      * cellular permission will restart the service. Wi-Fi disabled means that
      * the Wi-Fi manager is returning that Wi-Fi is not enabled, while in the
      * other case Wi-Fi is enabled but not available.
      */
-	static final int STATE_PAUSED_WIFI_DISABLED_NEED_CELLULAR_PERMISSION = 8;
-	static final int STATE_PAUSED_NEED_CELLULAR_PERMISSION = 9;
+    static final int STATE_PAUSED_WIFI_DISABLED_NEED_CELLULAR_PERMISSION = 8;
+    static final int STATE_PAUSED_NEED_CELLULAR_PERMISSION = 9;
 
-	/**
+    /**
      * Both STATE_PAUSED_WIFI_DISABLED and STATE_PAUSED_NEED_WIFI imply that
      * Wi-Fi is unavailable and cellular permission will NOT restart the
      * service. Wi-Fi disabled means that the Wi-Fi manager is returning that
@@ -53,27 +53,27 @@ public interface IDownloaderClient {
      * developers with very large payloads do not allow these payloads to be
      * downloaded over cellular connections.
      */
-	static final int STATE_PAUSED_WIFI_DISABLED = 10;
-	static final int STATE_PAUSED_NEED_WIFI = 11;
+    static final int STATE_PAUSED_WIFI_DISABLED = 10;
+    static final int STATE_PAUSED_NEED_WIFI = 11;
 
-	static final int STATE_PAUSED_ROAMING = 12;
+    static final int STATE_PAUSED_ROAMING = 12;
 
-	/**
+    /**
      * Scary case. We were on a network that redirected us to another website
      * that delivered us the wrong file.
      */
-	static final int STATE_PAUSED_NETWORK_SETUP_FAILURE = 13;
+    static final int STATE_PAUSED_NETWORK_SETUP_FAILURE = 13;
 
-	static final int STATE_PAUSED_SDCARD_UNAVAILABLE = 14;
+    static final int STATE_PAUSED_SDCARD_UNAVAILABLE = 14;
 
-	static final int STATE_FAILED_UNLICENSED = 15;
-	static final int STATE_FAILED_FETCHING_URL = 16;
-	static final int STATE_FAILED_SDCARD_FULL = 17;
-	static final int STATE_FAILED_CANCELED = 18;
+    static final int STATE_FAILED_UNLICENSED = 15;
+    static final int STATE_FAILED_FETCHING_URL = 16;
+    static final int STATE_FAILED_SDCARD_FULL = 17;
+    static final int STATE_FAILED_CANCELED = 18;
 
-	static final int STATE_FAILED = 19;
+    static final int STATE_FAILED = 19;
 
-	/**
+    /**
      * Called internally by the stub when the service is bound to the client.
      * <p>
      * Critical implementation detail. In onServiceConnected we create the
@@ -90,9 +90,9 @@ public interface IDownloaderClient {
      * @param m the service Messenger. This Messenger is used to call the
      *            service API from the client.
      */
-	void onServiceConnected(Messenger m);
+    void onServiceConnected(Messenger m);
 
-	/**
+    /**
      * Called when the download state changes. Depending on the state, there may
      * be user requests. The service is free to change the download state in the
      * middle of a user request, so the client should be able to handle this.
@@ -112,9 +112,9 @@ public interface IDownloaderClient {
      *
      * @param newState one of the STATE_* values defined in IDownloaderClient
      */
-	void onDownloadStateChanged(int newState);
+    void onDownloadStateChanged(int newState);
 
-	/**
+    /**
      * Shows the download progress. This is intended to be used to fill out a
      * client UI. This progress should only be shown in a few states such as
      * STATE_DOWNLOADING.
@@ -122,5 +122,5 @@ public interface IDownloaderClient {
      * @param progress the DownloadProgressInfo object containing the current
      *            progress of all downloads.
      */
-	void onDownloadProgress(DownloadProgressInfo progress);
+    void onDownloadProgress(DownloadProgressInfo progress);
 }

+ 14 - 14
platform/android/java/src/com/google/android/vending/expansion/downloader/IDownloaderService.java

@@ -31,47 +31,47 @@ import android.os.Messenger;
  * should immediately call {@link #onClientUpdated}.
  */
 public interface IDownloaderService {
-	/**
+    /**
      * Set this flag in response to the
      * IDownloaderClient.STATE_PAUSED_NEED_CELLULAR_PERMISSION state and then
      * call RequestContinueDownload to resume a download
      */
-	public static final int FLAGS_DOWNLOAD_OVER_CELLULAR = 1;
+    public static final int FLAGS_DOWNLOAD_OVER_CELLULAR = 1;
 
-	/**
+    /**
      * Request that the service abort the current download. The service should
      * respond by changing the state to {@link IDownloaderClient.STATE_ABORTED}.
      */
-	void requestAbortDownload();
+    void requestAbortDownload();
 
-	/**
+    /**
      * Request that the service pause the current download. The service should
      * respond by changing the state to
      * {@link IDownloaderClient.STATE_PAUSED_BY_REQUEST}.
      */
-	void requestPauseDownload();
+    void requestPauseDownload();
 
-	/**
+    /**
      * Request that the service continue a paused download, when in any paused
      * or failed state, including
      * {@link IDownloaderClient.STATE_PAUSED_BY_REQUEST}.
      */
-	void requestContinueDownload();
+    void requestContinueDownload();
 
-	/**
+    /**
      * Set the flags for this download (e.g.
      * {@link DownloaderService.FLAGS_DOWNLOAD_OVER_CELLULAR}).
      *
      * @param flags
      */
-	void setDownloadFlags(int flags);
+    void setDownloadFlags(int flags);
 
-	/**
+    /**
      * Requests that the download status be sent to the client.
      */
-	void requestDownloadStatus();
+    void requestDownloadStatus();
 
-	/**
+    /**
      * Call this when you get {@link
      * IDownloaderClient.onServiceConnected(Messenger m)} from the
      * DownloaderClient to register the client with the service. It will
@@ -79,5 +79,5 @@ public interface IDownloaderService {
      *
      * @param clientMessenger
      */
-	void onClientUpdated(Messenger clientMessenger);
+    void onClientUpdated(Messenger clientMessenger);
 }

+ 3 - 3
platform/android/java/src/com/google/android/vending/expansion/downloader/IStub.java

@@ -33,9 +33,9 @@ import android.os.Messenger;
  * {@link IDownloaderService#onClientUpdated}.
  */
 public interface IStub {
-	Messenger getMessenger();
+    Messenger getMessenger();
 
-	void connect(Context c);
+    void connect(Context c);
 
-	void disconnect(Context c);
+    void disconnect(Context c);
 }

+ 89 - 86
platform/android/java/src/com/google/android/vending/expansion/downloader/SystemFacade.java

@@ -16,7 +16,6 @@
 
 package com.google.android.vending.expansion.downloader;
 
-import android.annotation.SuppressLint;
 import android.app.Notification;
 import android.app.NotificationManager;
 import android.content.Context;
@@ -27,100 +26,104 @@ import android.net.NetworkInfo;
 import android.telephony.TelephonyManager;
 import android.util.Log;
 
+// -- GODOT start --
+import android.annotation.SuppressLint;
+// -- GODOT end --
+
 /**
  * Contains useful helper functions, typically tied to the application context.
  */
 class SystemFacade {
-	private Context mContext;
-	private NotificationManager mNotificationManager;
-
-	public SystemFacade(Context context) {
-		mContext = context;
-		mNotificationManager = (NotificationManager)
-									   mContext.getSystemService(Context.NOTIFICATION_SERVICE);
-	}
-
-	public long currentTimeMillis() {
-		return System.currentTimeMillis();
-	}
-
-	public Integer getActiveNetworkType() {
-		ConnectivityManager connectivity =
-				(ConnectivityManager)mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
-		if (connectivity == null) {
-			Log.w(Constants.TAG, "couldn't get connectivity manager");
-			return null;
-		}
-
-		@SuppressLint("MissingPermission")
-		NetworkInfo activeInfo = connectivity.getActiveNetworkInfo();
-		if (activeInfo == null) {
-			if (Constants.LOGVV) {
-				Log.v(Constants.TAG, "network is not available");
-			}
-			return null;
-		}
-		return activeInfo.getType();
-	}
-
-	public boolean isNetworkRoaming() {
-		ConnectivityManager connectivity =
-				(ConnectivityManager)mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
-		if (connectivity == null) {
-			Log.w(Constants.TAG, "couldn't get connectivity manager");
-			return false;
-		}
-
-		@SuppressLint("MissingPermission")
-		NetworkInfo info = connectivity.getActiveNetworkInfo();
-		boolean isMobile = (info != null && info.getType() == ConnectivityManager.TYPE_MOBILE);
-		TelephonyManager tm = (TelephonyManager)mContext
-									  .getSystemService(Context.TELEPHONY_SERVICE);
-		if (null == tm) {
-			Log.w(Constants.TAG, "couldn't get telephony manager");
-			return false;
-		}
-		boolean isRoaming = isMobile && tm.isNetworkRoaming();
-		if (Constants.LOGVV && isRoaming) {
-			Log.v(Constants.TAG, "network is roaming");
-		}
-		return isRoaming;
-	}
-
-	public Long getMaxBytesOverMobile() {
-		return (long)Integer.MAX_VALUE;
-	}
-
-	public Long getRecommendedMaxBytesOverMobile() {
-		return 2097152L;
-	}
-
-	public void sendBroadcast(Intent intent) {
-		mContext.sendBroadcast(intent);
-	}
-
-	public boolean userOwnsPackage(int uid, String packageName) throws NameNotFoundException {
-		return mContext.getPackageManager().getApplicationInfo(packageName, 0).uid == uid;
-	}
-
-	public void postNotification(long id, Notification notification) {
-		/**
+    private Context mContext;
+    private NotificationManager mNotificationManager;
+
+    public SystemFacade(Context context) {
+        mContext = context;
+        mNotificationManager = (NotificationManager)
+                mContext.getSystemService(Context.NOTIFICATION_SERVICE);
+    }
+
+    public long currentTimeMillis() {
+        return System.currentTimeMillis();
+    }
+
+    public Integer getActiveNetworkType() {
+        ConnectivityManager connectivity =
+                (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
+        if (connectivity == null) {
+            Log.w(Constants.TAG, "couldn't get connectivity manager");
+            return null;
+        }
+
+        @SuppressLint("MissingPermission")
+        NetworkInfo activeInfo = connectivity.getActiveNetworkInfo();
+        if (activeInfo == null) {
+            if (Constants.LOGVV) {
+                Log.v(Constants.TAG, "network is not available");
+            }
+            return null;
+        }
+        return activeInfo.getType();
+    }
+
+    public boolean isNetworkRoaming() {
+        ConnectivityManager connectivity =
+                (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
+        if (connectivity == null) {
+            Log.w(Constants.TAG, "couldn't get connectivity manager");
+            return false;
+        }
+
+        @SuppressLint("MissingPermission")
+        NetworkInfo info = connectivity.getActiveNetworkInfo();
+        boolean isMobile = (info != null && info.getType() == ConnectivityManager.TYPE_MOBILE);
+        TelephonyManager tm = (TelephonyManager) mContext
+                .getSystemService(Context.TELEPHONY_SERVICE);
+        if (null == tm) {
+            Log.w(Constants.TAG, "couldn't get telephony manager");
+            return false;
+        }
+        boolean isRoaming = isMobile && tm.isNetworkRoaming();
+        if (Constants.LOGVV && isRoaming) {
+            Log.v(Constants.TAG, "network is roaming");
+        }
+        return isRoaming;
+    }
+
+    public Long getMaxBytesOverMobile() {
+        return (long) Integer.MAX_VALUE;
+    }
+
+    public Long getRecommendedMaxBytesOverMobile() {
+        return 2097152L;
+    }
+
+    public void sendBroadcast(Intent intent) {
+        mContext.sendBroadcast(intent);
+    }
+
+    public boolean userOwnsPackage(int uid, String packageName) throws NameNotFoundException {
+        return mContext.getPackageManager().getApplicationInfo(packageName, 0).uid == uid;
+    }
+
+    public void postNotification(long id, Notification notification) {
+        /**
          * TODO: The system notification manager takes ints, not longs, as IDs,
          * but the download manager uses IDs take straight from the database,
          * which are longs. This will have to be dealt with at some point.
          */
-		mNotificationManager.notify((int)id, notification);
-	}
+        mNotificationManager.notify((int) id, notification);
+    }
 
-	public void cancelNotification(long id) {
-		mNotificationManager.cancel((int)id);
-	}
+    public void cancelNotification(long id) {
+        mNotificationManager.cancel((int) id);
+    }
 
-	public void cancelAllNotifications() {
-		mNotificationManager.cancelAll();
-	}
+    public void cancelAllNotifications() {
+        mNotificationManager.cancelAll();
+    }
 
-	public void startThread(Thread thread) {
-		thread.start();
-	}
+    public void startThread(Thread thread) {
+        thread.start();
+    }
 }

+ 66 - 65
platform/android/java/src/com/google/android/vending/expansion/downloader/impl/CustomIntentService.java

@@ -32,80 +32,81 @@ import android.util.Log;
  * intent, it does not queue up batches of intents of the same type.
  */
 public abstract class CustomIntentService extends Service {
-	private String mName;
-	private boolean mRedelivery;
-	private volatile ServiceHandler mServiceHandler;
-	private volatile Looper mServiceLooper;
-	private static final String LOG_TAG = "CustomIntentService";
-	private static final int WHAT_MESSAGE = -10;
+    private String mName;
+    private boolean mRedelivery;
+    private volatile ServiceHandler mServiceHandler;
+    private volatile Looper mServiceLooper;
+    private static final String LOG_TAG = "CustomIntentService";
+    private static final int WHAT_MESSAGE = -10;
 
-	public CustomIntentService(String paramString) {
-		this.mName = paramString;
-	}
+    public CustomIntentService(String paramString) {
+        this.mName = paramString;
+    }
 
-	@Override
-	public IBinder onBind(Intent paramIntent) {
-		return null;
-	}
+    @Override
+    public IBinder onBind(Intent paramIntent) {
+        return null;
+    }
 
-	@Override
-	public void onCreate() {
-		super.onCreate();
-		HandlerThread localHandlerThread = new HandlerThread("IntentService[" + this.mName + "]");
-		localHandlerThread.start();
-		this.mServiceLooper = localHandlerThread.getLooper();
-		this.mServiceHandler = new ServiceHandler(this.mServiceLooper);
-	}
+    @Override
+    public void onCreate() {
+        super.onCreate();
+        HandlerThread localHandlerThread = new HandlerThread("IntentService["
+                + this.mName + "]");
+        localHandlerThread.start();
+        this.mServiceLooper = localHandlerThread.getLooper();
+        this.mServiceHandler = new ServiceHandler(this.mServiceLooper);
+    }
 
-	@Override
-	public void onDestroy() {
-		Thread localThread = this.mServiceLooper.getThread();
-		if ((localThread != null) && (localThread.isAlive())) {
-			localThread.interrupt();
-		}
-		this.mServiceLooper.quit();
-		Log.d(LOG_TAG, "onDestroy");
-	}
+    @Override
+    public void onDestroy() {
+        Thread localThread = this.mServiceLooper.getThread();
+        if ((localThread != null) && (localThread.isAlive())) {
+            localThread.interrupt();
+        }
+        this.mServiceLooper.quit();
+        Log.d(LOG_TAG, "onDestroy");
+    }
 
-	protected abstract void onHandleIntent(Intent paramIntent);
+    protected abstract void onHandleIntent(Intent paramIntent);
 
-	protected abstract boolean shouldStop();
+    protected abstract boolean shouldStop();
 
-	@Override
-	public void onStart(Intent paramIntent, int startId) {
-		if (!this.mServiceHandler.hasMessages(WHAT_MESSAGE)) {
-			Message localMessage = this.mServiceHandler.obtainMessage();
-			localMessage.arg1 = startId;
-			localMessage.obj = paramIntent;
-			localMessage.what = WHAT_MESSAGE;
-			this.mServiceHandler.sendMessage(localMessage);
-		}
-	}
+    @Override
+    public void onStart(Intent paramIntent, int startId) {
+        if (!this.mServiceHandler.hasMessages(WHAT_MESSAGE)) {
+            Message localMessage = this.mServiceHandler.obtainMessage();
+            localMessage.arg1 = startId;
+            localMessage.obj = paramIntent;
+            localMessage.what = WHAT_MESSAGE;
+            this.mServiceHandler.sendMessage(localMessage);
+        }
+    }
 
-	@Override
-	public int onStartCommand(Intent paramIntent, int flags, int startId) {
-		onStart(paramIntent, startId);
-		return mRedelivery ? START_REDELIVER_INTENT : START_NOT_STICKY;
-	}
+    @Override
+    public int onStartCommand(Intent paramIntent, int flags, int startId) {
+        onStart(paramIntent, startId);
+        return mRedelivery ? START_REDELIVER_INTENT : START_NOT_STICKY;
+    }
 
-	public void setIntentRedelivery(boolean enabled) {
-		this.mRedelivery = enabled;
-	}
+    public void setIntentRedelivery(boolean enabled) {
+        this.mRedelivery = enabled;
+    }
 
-	private final class ServiceHandler extends Handler {
-		public ServiceHandler(Looper looper) {
-			super(looper);
-		}
+    private final class ServiceHandler extends Handler {
+        public ServiceHandler(Looper looper) {
+            super(looper);
+        }
 
-		@Override
-		public void handleMessage(Message paramMessage) {
-			CustomIntentService.this
-					.onHandleIntent((Intent)paramMessage.obj);
-			if (shouldStop()) {
-				Log.d(LOG_TAG, "stopSelf");
-				CustomIntentService.this.stopSelf(paramMessage.arg1);
-				Log.d(LOG_TAG, "afterStopSelf");
-			}
-		}
-	}
+        @Override
+        public void handleMessage(Message paramMessage) {
+            CustomIntentService.this
+                    .onHandleIntent((Intent) paramMessage.obj);
+            if (shouldStop()) {
+                Log.d(LOG_TAG, "stopSelf");
+                CustomIntentService.this.stopSelf(paramMessage.arg1);
+                Log.d(LOG_TAG, "afterStopSelf");
+            }
+        }
+    }
 }

+ 56 - 56
platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadInfo.java

@@ -25,68 +25,68 @@ import android.util.Log;
  * Representation of information about an individual download from the database.
  */
 public class DownloadInfo {
-	public String mUri;
-	public final int mIndex;
-	public final String mFileName;
-	public String mETag;
-	public long mTotalBytes;
-	public long mCurrentBytes;
-	public long mLastMod;
-	public int mStatus;
-	public int mControl;
-	public int mNumFailed;
-	public int mRetryAfter;
-	public int mRedirectCount;
+    public String mUri;
+    public final int mIndex;
+    public final String mFileName;
+    public String mETag;
+    public long mTotalBytes;
+    public long mCurrentBytes;
+    public long mLastMod;
+    public int mStatus;
+    public int mControl;
+    public int mNumFailed;
+    public int mRetryAfter;
+    public int mRedirectCount;
 
-	boolean mInitialized;
+    boolean mInitialized;
 
-	public int mFuzz;
+    public int mFuzz;
 
-	public DownloadInfo(int index, String fileName, String pkg) {
-		mFuzz = Helpers.sRandom.nextInt(1001);
-		mFileName = fileName;
-		mIndex = index;
-	}
+    public DownloadInfo(int index, String fileName, String pkg) {
+        mFuzz = Helpers.sRandom.nextInt(1001);
+        mFileName = fileName;
+        mIndex = index;
+    }
 
-	public void resetDownload() {
-		mCurrentBytes = 0;
-		mETag = "";
-		mLastMod = 0;
-		mStatus = 0;
-		mControl = 0;
-		mNumFailed = 0;
-		mRetryAfter = 0;
-		mRedirectCount = 0;
-	}
+    public void resetDownload() {
+        mCurrentBytes = 0;
+        mETag = "";
+        mLastMod = 0;
+        mStatus = 0;
+        mControl = 0;
+        mNumFailed = 0;
+        mRetryAfter = 0;
+        mRedirectCount = 0;
+    }
 
-	/**
+    /**
      * Returns the time when a download should be restarted.
      */
-	public long restartTime(long now) {
-		if (mNumFailed == 0) {
-			return now;
-		}
-		if (mRetryAfter > 0) {
-			return mLastMod + mRetryAfter;
-		}
-		return mLastMod +
-				Constants.RETRY_FIRST_DELAY *
-						(1000 + mFuzz) * (1 << (mNumFailed - 1));
-	}
+    public long restartTime(long now) {
+        if (mNumFailed == 0) {
+            return now;
+        }
+        if (mRetryAfter > 0) {
+            return mLastMod + mRetryAfter;
+        }
+        return mLastMod +
+                Constants.RETRY_FIRST_DELAY *
+                (1000 + mFuzz) * (1 << (mNumFailed - 1));
+    }
 
-	public void logVerboseInfo() {
-		Log.v(Constants.TAG, "Service adding new entry");
-		Log.v(Constants.TAG, "FILENAME: " + mFileName);
-		Log.v(Constants.TAG, "URI     : " + mUri);
-		Log.v(Constants.TAG, "FILENAME: " + mFileName);
-		Log.v(Constants.TAG, "CONTROL : " + mControl);
-		Log.v(Constants.TAG, "STATUS  : " + mStatus);
-		Log.v(Constants.TAG, "FAILED_C: " + mNumFailed);
-		Log.v(Constants.TAG, "RETRY_AF: " + mRetryAfter);
-		Log.v(Constants.TAG, "REDIRECT: " + mRedirectCount);
-		Log.v(Constants.TAG, "LAST_MOD: " + mLastMod);
-		Log.v(Constants.TAG, "TOTAL   : " + mTotalBytes);
-		Log.v(Constants.TAG, "CURRENT : " + mCurrentBytes);
-		Log.v(Constants.TAG, "ETAG    : " + mETag);
-	}
+    public void logVerboseInfo() {
+        Log.v(Constants.TAG, "Service adding new entry");
+        Log.v(Constants.TAG, "FILENAME: " + mFileName);
+        Log.v(Constants.TAG, "URI     : " + mUri);
+        Log.v(Constants.TAG, "FILENAME: " + mFileName);
+        Log.v(Constants.TAG, "CONTROL : " + mControl);
+        Log.v(Constants.TAG, "STATUS  : " + mStatus);
+        Log.v(Constants.TAG, "FAILED_C: " + mNumFailed);
+        Log.v(Constants.TAG, "RETRY_AF: " + mRetryAfter);
+        Log.v(Constants.TAG, "REDIRECT: " + mRedirectCount);
+        Log.v(Constants.TAG, "LAST_MOD: " + mLastMod);
+        Log.v(Constants.TAG, "TOTAL   : " + mTotalBytes);
+        Log.v(Constants.TAG, "CURRENT : " + mCurrentBytes);
+        Log.v(Constants.TAG, "ETAG    : " + mETag);
+    }
 }

+ 174 - 169
platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadNotification.java

@@ -16,7 +16,11 @@
 
 package com.google.android.vending.expansion.downloader.impl;
 
+// -- GODOT start --
+//import com.android.vending.expansion.downloader.R;
 import com.godot.game.R;
+// -- GODOT end --
+
 import com.google.android.vending.expansion.downloader.DownloadProgressInfo;
 import com.google.android.vending.expansion.downloader.DownloaderClientMarshaller;
 import com.google.android.vending.expansion.downloader.Helpers;
@@ -42,183 +46,184 @@ import android.support.v4.app.NotificationCompat;
  */
 public class DownloadNotification implements IDownloaderClient {
 
-	private int mState;
-	private final Context mContext;
-	private final NotificationManager mNotificationManager;
-	private CharSequence mCurrentTitle;
-
-	private IDownloaderClient mClientProxy;
-	private NotificationCompat.Builder mActiveDownloadBuilder;
-	private NotificationCompat.Builder mBuilder;
-	private NotificationCompat.Builder mCurrentBuilder;
-	private CharSequence mLabel;
-	private String mCurrentText;
-	private DownloadProgressInfo mProgressInfo;
-	private PendingIntent mContentIntent;
-
-	static final String LOGTAG = "DownloadNotification";
-	static final int NOTIFICATION_ID = LOGTAG.hashCode();
-
-	public PendingIntent getClientIntent() {
-		return mContentIntent;
-	}
-
-	public void setClientIntent(PendingIntent clientIntent) {
-		this.mBuilder.setContentIntent(clientIntent);
-		this.mActiveDownloadBuilder.setContentIntent(clientIntent);
-		this.mContentIntent = clientIntent;
-	}
-
-	public void resendState() {
-		if (null != mClientProxy) {
-			mClientProxy.onDownloadStateChanged(mState);
-		}
-	}
-
-	@Override
-	public void onDownloadStateChanged(int newState) {
-		if (null != mClientProxy) {
-			mClientProxy.onDownloadStateChanged(newState);
-		}
-		if (newState != mState) {
-			mState = newState;
-			if (newState == IDownloaderClient.STATE_IDLE || null == mContentIntent) {
-				return;
-			}
-			int stringDownloadID;
-			int iconResource;
-			boolean ongoingEvent;
-
-			// get the new title string and paused text
-			switch (newState) {
-				case 0:
-					iconResource = android.R.drawable.stat_sys_warning;
-					stringDownloadID = R.string.state_unknown;
-					ongoingEvent = false;
-					break;
-
-				case IDownloaderClient.STATE_DOWNLOADING:
-					iconResource = android.R.drawable.stat_sys_download;
-					stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState);
-					ongoingEvent = true;
-					break;
-
-				case IDownloaderClient.STATE_FETCHING_URL:
-				case IDownloaderClient.STATE_CONNECTING:
-					iconResource = android.R.drawable.stat_sys_download_done;
-					stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState);
-					ongoingEvent = true;
-					break;
-
-				case IDownloaderClient.STATE_COMPLETED:
-				case IDownloaderClient.STATE_PAUSED_BY_REQUEST:
-					iconResource = android.R.drawable.stat_sys_download_done;
-					stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState);
-					ongoingEvent = false;
-					break;
-
-				case IDownloaderClient.STATE_FAILED:
-				case IDownloaderClient.STATE_FAILED_CANCELED:
-				case IDownloaderClient.STATE_FAILED_FETCHING_URL:
-				case IDownloaderClient.STATE_FAILED_SDCARD_FULL:
-				case IDownloaderClient.STATE_FAILED_UNLICENSED:
-					iconResource = android.R.drawable.stat_sys_warning;
-					stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState);
-					ongoingEvent = false;
-					break;
-
-				default:
-					iconResource = android.R.drawable.stat_sys_warning;
-					stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState);
-					ongoingEvent = true;
-					break;
-			}
-
-			mCurrentText = mContext.getString(stringDownloadID);
-			mCurrentTitle = mLabel;
-			mCurrentBuilder.setTicker(mLabel + ": " + mCurrentText);
-			mCurrentBuilder.setSmallIcon(iconResource);
-			mCurrentBuilder.setContentTitle(mCurrentTitle);
-			mCurrentBuilder.setContentText(mCurrentText);
-			if (ongoingEvent) {
-				mCurrentBuilder.setOngoing(true);
-			} else {
-				mCurrentBuilder.setOngoing(false);
-				mCurrentBuilder.setAutoCancel(true);
-			}
-			mNotificationManager.notify(NOTIFICATION_ID, mCurrentBuilder.build());
-		}
-	}
-
-	@Override
-	public void onDownloadProgress(DownloadProgressInfo progress) {
-		mProgressInfo = progress;
-		if (null != mClientProxy) {
-			mClientProxy.onDownloadProgress(progress);
-		}
-		if (progress.mOverallTotal <= 0) {
-			// we just show the text
-			mBuilder.setTicker(mCurrentTitle);
-			mBuilder.setSmallIcon(android.R.drawable.stat_sys_download);
-			mBuilder.setContentTitle(mCurrentTitle);
-			mBuilder.setContentText(mCurrentText);
-			mCurrentBuilder = mBuilder;
-		} else {
-			mActiveDownloadBuilder.setProgress((int)progress.mOverallTotal, (int)progress.mOverallProgress, false);
-			mActiveDownloadBuilder.setContentText(Helpers.getDownloadProgressString(progress.mOverallProgress, progress.mOverallTotal));
-			mActiveDownloadBuilder.setSmallIcon(android.R.drawable.stat_sys_download);
-			mActiveDownloadBuilder.setTicker(mLabel + ": " + mCurrentText);
-			mActiveDownloadBuilder.setContentTitle(mLabel);
-			mActiveDownloadBuilder.setContentInfo(mContext.getString(R.string.time_remaining_notification,
-					Helpers.getTimeRemaining(progress.mTimeRemaining)));
-			mCurrentBuilder = mActiveDownloadBuilder;
-		}
-		mNotificationManager.notify(NOTIFICATION_ID, mCurrentBuilder.build());
-	}
-
-	/**
+    private int mState;
+    private final Context mContext;
+    private final NotificationManager mNotificationManager;
+    private CharSequence mCurrentTitle;
+
+    private IDownloaderClient mClientProxy;
+    private NotificationCompat.Builder mActiveDownloadBuilder;
+    private NotificationCompat.Builder mBuilder;
+    private NotificationCompat.Builder mCurrentBuilder;
+    private CharSequence mLabel;
+    private String mCurrentText;
+    private DownloadProgressInfo mProgressInfo;
+    private PendingIntent mContentIntent;
+
+    static final String LOGTAG = "DownloadNotification";
+    static final int NOTIFICATION_ID = LOGTAG.hashCode();
+
+    public PendingIntent getClientIntent() {
+        return mContentIntent;
+    }
+
+    public void setClientIntent(PendingIntent clientIntent) {
+        this.mBuilder.setContentIntent(clientIntent);
+        this.mActiveDownloadBuilder.setContentIntent(clientIntent);
+        this.mContentIntent = clientIntent;
+    }
+
+    public void resendState() {
+        if (null != mClientProxy) {
+            mClientProxy.onDownloadStateChanged(mState);
+        }
+    }
+
+    @Override
+    public void onDownloadStateChanged(int newState) {
+        if (null != mClientProxy) {
+            mClientProxy.onDownloadStateChanged(newState);
+        }
+        if (newState != mState) {
+            mState = newState;
+            if (newState == IDownloaderClient.STATE_IDLE || null == mContentIntent) {
+                return;
+            }
+            int stringDownloadID;
+            int iconResource;
+            boolean ongoingEvent;
+
+            // get the new title string and paused text
+            switch (newState) {
+                case 0:
+                    iconResource = android.R.drawable.stat_sys_warning;
+                    stringDownloadID = R.string.state_unknown;
+                    ongoingEvent = false;
+                    break;
+
+                case IDownloaderClient.STATE_DOWNLOADING:
+                    iconResource = android.R.drawable.stat_sys_download;
+                    stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState);
+                    ongoingEvent = true;
+                    break;
+
+                case IDownloaderClient.STATE_FETCHING_URL:
+                case IDownloaderClient.STATE_CONNECTING:
+                    iconResource = android.R.drawable.stat_sys_download_done;
+                    stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState);
+                    ongoingEvent = true;
+                    break;
+
+                case IDownloaderClient.STATE_COMPLETED:
+                case IDownloaderClient.STATE_PAUSED_BY_REQUEST:
+                    iconResource = android.R.drawable.stat_sys_download_done;
+                    stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState);
+                    ongoingEvent = false;
+                    break;
+
+                case IDownloaderClient.STATE_FAILED:
+                case IDownloaderClient.STATE_FAILED_CANCELED:
+                case IDownloaderClient.STATE_FAILED_FETCHING_URL:
+                case IDownloaderClient.STATE_FAILED_SDCARD_FULL:
+                case IDownloaderClient.STATE_FAILED_UNLICENSED:
+                    iconResource = android.R.drawable.stat_sys_warning;
+                    stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState);
+                    ongoingEvent = false;
+                    break;
+
+                default:
+                    iconResource = android.R.drawable.stat_sys_warning;
+                    stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState);
+                    ongoingEvent = true;
+                    break;
+            }
+
+            mCurrentText = mContext.getString(stringDownloadID);
+            mCurrentTitle = mLabel;
+            mCurrentBuilder.setTicker(mLabel + ": " + mCurrentText);
+            mCurrentBuilder.setSmallIcon(iconResource);
+            mCurrentBuilder.setContentTitle(mCurrentTitle);
+            mCurrentBuilder.setContentText(mCurrentText);
+            if (ongoingEvent) {
+                mCurrentBuilder.setOngoing(true);
+            } else {
+                mCurrentBuilder.setOngoing(false);
+                mCurrentBuilder.setAutoCancel(true);
+            }
+            mNotificationManager.notify(NOTIFICATION_ID, mCurrentBuilder.build());
+        }
+    }
+
+    @Override
+    public void onDownloadProgress(DownloadProgressInfo progress) {
+        mProgressInfo = progress;
+        if (null != mClientProxy) {
+            mClientProxy.onDownloadProgress(progress);
+        }
+        if (progress.mOverallTotal <= 0) {
+            // we just show the text
+            mBuilder.setTicker(mCurrentTitle);
+            mBuilder.setSmallIcon(android.R.drawable.stat_sys_download);
+            mBuilder.setContentTitle(mCurrentTitle);
+            mBuilder.setContentText(mCurrentText);
+            mCurrentBuilder = mBuilder;
+        } else {
+            mActiveDownloadBuilder.setProgress((int) progress.mOverallTotal, (int) progress.mOverallProgress, false);
+            mActiveDownloadBuilder.setContentText(Helpers.getDownloadProgressString(progress.mOverallProgress, progress.mOverallTotal));
+            mActiveDownloadBuilder.setSmallIcon(android.R.drawable.stat_sys_download);
+            mActiveDownloadBuilder.setTicker(mLabel + ": " + mCurrentText);
+            mActiveDownloadBuilder.setContentTitle(mLabel);
+            mActiveDownloadBuilder.setContentInfo(mContext.getString(R.string.time_remaining_notification,
+                    Helpers.getTimeRemaining(progress.mTimeRemaining)));
+            mCurrentBuilder = mActiveDownloadBuilder;
+        }
+        mNotificationManager.notify(NOTIFICATION_ID, mCurrentBuilder.build());
+    }
+
+    /**
      * Called in response to onClientUpdated. Creates a new proxy and notifies
      * it of the current state.
      *
      * @param msg the client Messenger to notify
      */
-	public void setMessenger(Messenger msg) {
-		mClientProxy = DownloaderClientMarshaller.CreateProxy(msg);
-		if (null != mProgressInfo) {
-			mClientProxy.onDownloadProgress(mProgressInfo);
-		}
-		if (mState != -1) {
-			mClientProxy.onDownloadStateChanged(mState);
-		}
-	}
-
-	/**
+    public void setMessenger(Messenger msg) {
+        mClientProxy = DownloaderClientMarshaller.CreateProxy(msg);
+        if (null != mProgressInfo) {
+            mClientProxy.onDownloadProgress(mProgressInfo);
+        }
+        if (mState != -1) {
+            mClientProxy.onDownloadStateChanged(mState);
+        }
+    }
+
+    /**
      * Constructor
      *
      * @param ctx The context to use to obtain access to the Notification
      *            Service
      */
-	DownloadNotification(Context ctx, CharSequence applicationLabel) {
-		mState = -1;
-		mContext = ctx;
-		mLabel = applicationLabel;
-		mNotificationManager = (NotificationManager)
-									   mContext.getSystemService(Context.NOTIFICATION_SERVICE);
-		mActiveDownloadBuilder = new NotificationCompat.Builder(ctx);
-		mBuilder = new NotificationCompat.Builder(ctx);
-
-		// Set Notification category and priorities to something that makes sense for a long
-		// lived background task.
-		mActiveDownloadBuilder.setPriority(NotificationCompat.PRIORITY_LOW);
-		mActiveDownloadBuilder.setCategory(NotificationCompat.CATEGORY_PROGRESS);
-
-		mBuilder.setPriority(NotificationCompat.PRIORITY_LOW);
-		mBuilder.setCategory(NotificationCompat.CATEGORY_PROGRESS);
-
-		mCurrentBuilder = mBuilder;
-	}
-
-	@Override
-	public void onServiceConnected(Messenger m) {
-	}
+    DownloadNotification(Context ctx, CharSequence applicationLabel) {
+        mState = -1;
+        mContext = ctx;
+        mLabel = applicationLabel;
+        mNotificationManager = (NotificationManager)
+                mContext.getSystemService(Context.NOTIFICATION_SERVICE);
+        mActiveDownloadBuilder = new NotificationCompat.Builder(ctx);
+        mBuilder = new NotificationCompat.Builder(ctx);
+
+        // Set Notification category and priorities to something that makes sense for a long
+        // lived background task.
+        mActiveDownloadBuilder.setPriority(NotificationCompat.PRIORITY_LOW);
+        mActiveDownloadBuilder.setCategory(NotificationCompat.CATEGORY_PROGRESS);
+
+        mBuilder.setPriority(NotificationCompat.PRIORITY_LOW);
+        mBuilder.setCategory(NotificationCompat.CATEGORY_PROGRESS);
+
+        mCurrentBuilder = mBuilder;
+    }
+
+    @Override
+    public void onServiceConnected(Messenger m) {
+    }
+
 }

+ 710 - 691
platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadThread.java

@@ -40,435 +40,447 @@ import java.util.Locale;
  */
 public class DownloadThread {
 
-	private Context mContext;
-	private DownloadInfo mInfo;
-	private DownloaderService mService;
-	private final DownloadsDB mDB;
-	private final DownloadNotification mNotification;
-	private String mUserAgent;
-
-	public DownloadThread(DownloadInfo info, DownloaderService service,
-			DownloadNotification notification) {
-		mContext = service;
-		mInfo = info;
-		mService = service;
-		mNotification = notification;
-		mDB = DownloadsDB.getDB(service);
-		mUserAgent = "APKXDL (Linux; U; Android " + android.os.Build.VERSION.RELEASE + ";" + Locale.getDefault().toString() + "; " + android.os.Build.DEVICE + "/" + android.os.Build.ID + ")" +
-					 service.getPackageName();
-	}
-
-	/**
+    private Context mContext;
+    private DownloadInfo mInfo;
+    private DownloaderService mService;
+    private final DownloadsDB mDB;
+    private final DownloadNotification mNotification;
+    private String mUserAgent;
+
+    public DownloadThread(DownloadInfo info, DownloaderService service,
+            DownloadNotification notification) {
+        mContext = service;
+        mInfo = info;
+        mService = service;
+        mNotification = notification;
+        mDB = DownloadsDB.getDB(service);
+        mUserAgent = "APKXDL (Linux; U; Android " + android.os.Build.VERSION.RELEASE + ";"
+                + Locale.getDefault().toString() + "; " + android.os.Build.DEVICE + "/"
+                + android.os.Build.ID + ")" +
+                service.getPackageName();
+    }
+
+    /**
      * Returns the default user agent
      */
-	private String userAgent() {
-		return mUserAgent;
-	}
+    private String userAgent() {
+        return mUserAgent;
+    }
 
-	/**
+    /**
      * State for the entire run() method.
      */
-	private static class State {
-		public String mFilename;
-		public FileOutputStream mStream;
-		public boolean mCountRetry = false;
-		public int mRetryAfter = 0;
-		public int mRedirectCount = 0;
-		public String mNewUri;
-		public boolean mGotData = false;
-		public String mRequestUri;
-
-		public State(DownloadInfo info, DownloaderService service) {
-			mRedirectCount = info.mRedirectCount;
-			mRequestUri = info.mUri;
-			mFilename = service.generateTempSaveFileName(info.mFileName);
-		}
-	}
-
-	/**
+    private static class State {
+        public String mFilename;
+        public FileOutputStream mStream;
+        public boolean mCountRetry = false;
+        public int mRetryAfter = 0;
+        public int mRedirectCount = 0;
+        public String mNewUri;
+        public boolean mGotData = false;
+        public String mRequestUri;
+
+        public State(DownloadInfo info, DownloaderService service) {
+            mRedirectCount = info.mRedirectCount;
+            mRequestUri = info.mUri;
+            mFilename = service.generateTempSaveFileName(info.mFileName);
+        }
+    }
+
+    /**
      * State within executeDownload()
      */
-	private static class InnerState {
-		public int mBytesSoFar = 0;
-		public int mBytesThisSession = 0;
-		public String mHeaderETag;
-		public boolean mContinuingDownload = false;
-		public String mHeaderContentLength;
-		public String mHeaderContentDisposition;
-		public String mHeaderContentLocation;
-		public int mBytesNotified = 0;
-		public long mTimeLastNotification = 0;
-	}
-
-	/**
+    private static class InnerState {
+        public int mBytesSoFar = 0;
+        public int mBytesThisSession = 0;
+        public String mHeaderETag;
+        public boolean mContinuingDownload = false;
+        public String mHeaderContentLength;
+        public String mHeaderContentDisposition;
+        public String mHeaderContentLocation;
+        public int mBytesNotified = 0;
+        public long mTimeLastNotification = 0;
+    }
+
+    /**
      * Raised from methods called by run() to indicate that the current request
      * should be stopped immediately. Note the message passed to this exception
      * will be logged and therefore must be guaranteed not to contain any PII,
      * meaning it generally can't include any information about the request URI,
      * headers, or destination filename.
      */
-	private class StopRequest extends Throwable {
-
-		private static final long serialVersionUID = 6338592678988347973L;
-		public int mFinalStatus;
-
-		public StopRequest(int finalStatus, String message) {
-			super(message);
-			mFinalStatus = finalStatus;
-		}
-
-		public StopRequest(int finalStatus, String message, Throwable throwable) {
-			super(message, throwable);
-			mFinalStatus = finalStatus;
-		}
-	}
-
-	/**
+    private class StopRequest extends Throwable {
+    	
+        private static final long serialVersionUID = 6338592678988347973L;
+        public int mFinalStatus;
+
+        public StopRequest(int finalStatus, String message) {
+            super(message);
+            mFinalStatus = finalStatus;
+        }
+
+        public StopRequest(int finalStatus, String message, Throwable throwable) {
+            super(message, throwable);
+            mFinalStatus = finalStatus;
+        }
+    }
+
+    /**
      * Raised from methods called by executeDownload() to indicate that the
      * download should be retried immediately.
      */
-	private class RetryDownload extends Throwable {
+    private class RetryDownload extends Throwable {
 
-		private static final long serialVersionUID = 6196036036517540229L;
-	}
+        private static final long serialVersionUID = 6196036036517540229L;
+    }
 
-	/**
+    /**
      * Executes the download in a separate thread
      */
-	public void run() {
-		Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
-
-		State state = new State(mInfo, mService);
-		PowerManager.WakeLock wakeLock = null;
-		int finalStatus = DownloaderService.STATUS_UNKNOWN_ERROR;
-
-		try {
-			PowerManager pm = (PowerManager)mContext.getSystemService(Context.POWER_SERVICE);
-			wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "org.godot.game:wakelock");
-			wakeLock.acquire(20 * 60 * 1000L /*20 minutes*/);
-
-			if (Constants.LOGV) {
-				Log.v(Constants.TAG, "initiating download for " + mInfo.mFileName);
-				Log.v(Constants.TAG, "  at " + mInfo.mUri);
-			}
-
-			boolean finished = false;
-			while (!finished) {
-				if (Constants.LOGV) {
-					Log.v(Constants.TAG, "initiating download for " + mInfo.mFileName);
-					Log.v(Constants.TAG, "  at " + mInfo.mUri);
-				}
-				// Set or unset proxy, which may have changed since last GET
-				// request.
-				// setDefaultProxy() supports null as proxy parameter.
-				URL url = new URL(state.mRequestUri);
-				HttpURLConnection request = (HttpURLConnection)url.openConnection();
-				request.setRequestProperty("User-Agent", userAgent());
-				try {
-					executeDownload(state, request);
-					finished = true;
-				} catch (RetryDownload exc) {
-					// fall through
-				} finally {
-					request.disconnect();
-					request = null;
-				}
-			}
-
-			if (Constants.LOGV) {
-				Log.v(Constants.TAG, "download completed for " + mInfo.mFileName);
-				Log.v(Constants.TAG, "  at " + mInfo.mUri);
-			}
-			finalizeDestinationFile(state);
-			finalStatus = DownloaderService.STATUS_SUCCESS;
-		} catch (StopRequest error) {
-			// remove the cause before printing, in case it contains PII
-			Log.w(Constants.TAG,
-					"Aborting request for download " + mInfo.mFileName + ": " + error.getMessage());
-			error.printStackTrace();
-			finalStatus = error.mFinalStatus;
-			// fall through to finally block
-		} catch (Throwable ex) { // sometimes the socket code throws unchecked
-			// exceptions
-			Log.w(Constants.TAG, "Exception for " + mInfo.mFileName + ": " + ex);
-			finalStatus = DownloaderService.STATUS_UNKNOWN_ERROR;
-			// falls through to the code that reports an error
-		} finally {
-			if (wakeLock != null) {
-				wakeLock.release();
-				wakeLock = null;
-			}
-			cleanupDestination(state, finalStatus);
-			notifyDownloadCompleted(finalStatus, state.mCountRetry, state.mRetryAfter,
-					state.mRedirectCount, state.mGotData, state.mFilename);
-		}
-	}
-
-	/**
+    public void run() {
+        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+
+        State state = new State(mInfo, mService);
+        PowerManager.WakeLock wakeLock = null;
+        int finalStatus = DownloaderService.STATUS_UNKNOWN_ERROR;
+
+        try {
+            PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
+            // -- GODOT start --
+            //wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, Constants.TAG);
+            //wakeLock.acquire();
+            wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "org.godot.game:wakelock");
+            wakeLock.acquire(20 * 60 * 1000L /*20 minutes*/);
+            // -- GODOT end --
+
+            if (Constants.LOGV) {
+                Log.v(Constants.TAG, "initiating download for " + mInfo.mFileName);
+                Log.v(Constants.TAG, "  at " + mInfo.mUri);
+            }
+
+            boolean finished = false;
+            while (!finished) {
+                if (Constants.LOGV) {
+                    Log.v(Constants.TAG, "initiating download for " + mInfo.mFileName);
+                    Log.v(Constants.TAG, "  at " + mInfo.mUri);
+                }
+                // Set or unset proxy, which may have changed since last GET
+                // request.
+                // setDefaultProxy() supports null as proxy parameter.
+                URL url = new URL(state.mRequestUri);
+                HttpURLConnection request = (HttpURLConnection)url.openConnection();
+                request.setRequestProperty("User-Agent", userAgent());
+                try {
+                    executeDownload(state, request);
+                    finished = true;
+                } catch (RetryDownload exc) {
+                    // fall through
+                } finally {
+                    request.disconnect();
+                    request = null;
+                }
+            }
+
+            if (Constants.LOGV) {
+                Log.v(Constants.TAG, "download completed for " + mInfo.mFileName);
+                Log.v(Constants.TAG, "  at " + mInfo.mUri);
+            }
+            finalizeDestinationFile(state);
+            finalStatus = DownloaderService.STATUS_SUCCESS;
+        } catch (StopRequest error) {
+            // remove the cause before printing, in case it contains PII
+            Log.w(Constants.TAG,
+                    "Aborting request for download " + mInfo.mFileName + ": " + error.getMessage());
+            error.printStackTrace();
+            finalStatus = error.mFinalStatus;
+            // fall through to finally block
+        } catch (Throwable ex) { // sometimes the socket code throws unchecked
+                                 // exceptions
+            Log.w(Constants.TAG, "Exception for " + mInfo.mFileName + ": " + ex);
+            finalStatus = DownloaderService.STATUS_UNKNOWN_ERROR;
+            // falls through to the code that reports an error
+        } finally {
+            if (wakeLock != null) {
+                wakeLock.release();
+                wakeLock = null;
+            }
+            cleanupDestination(state, finalStatus);
+            notifyDownloadCompleted(finalStatus, state.mCountRetry, state.mRetryAfter,
+                    state.mRedirectCount, state.mGotData, state.mFilename);
+        }
+    }
+
+    /**
      * Fully execute a single download request - setup and send the request,
      * handle the response, and transfer the data to the destination file.
      */
-	private void executeDownload(State state, HttpURLConnection request)
-			throws StopRequest, RetryDownload {
-		InnerState innerState = new InnerState();
-		byte data[] = new byte[Constants.BUFFER_SIZE];
+    private void executeDownload(State state, HttpURLConnection request)
+            throws StopRequest, RetryDownload {
+        InnerState innerState = new InnerState();
+        byte data[] = new byte[Constants.BUFFER_SIZE];
 
-		checkPausedOrCanceled(state);
+        checkPausedOrCanceled(state);
 
-		setupDestinationFile(state, innerState);
-		addRequestHeaders(innerState, request);
+        setupDestinationFile(state, innerState);
+        addRequestHeaders(innerState, request);
 
-		// check just before sending the request to avoid using an invalid
-		// connection at all
-		checkConnectivity(state);
+        // check just before sending the request to avoid using an invalid
+        // connection at all
+        checkConnectivity(state);
 
-		mNotification.onDownloadStateChanged(IDownloaderClient.STATE_CONNECTING);
-		int responseCode = sendRequest(state, request);
-		handleExceptionalStatus(state, innerState, request, responseCode);
+        mNotification.onDownloadStateChanged(IDownloaderClient.STATE_CONNECTING);
+        int responseCode = sendRequest(state, request);
+        handleExceptionalStatus(state, innerState, request, responseCode);
 
-		if (Constants.LOGV) {
-			Log.v(Constants.TAG, "received response for " + mInfo.mUri);
-		}
+        if (Constants.LOGV) {
+            Log.v(Constants.TAG, "received response for " + mInfo.mUri);
+        }
 
-		processResponseHeaders(state, innerState, request);
-		InputStream entityStream = openResponseEntity(state, request);
-		mNotification.onDownloadStateChanged(IDownloaderClient.STATE_DOWNLOADING);
-		transferData(state, innerState, data, entityStream);
-	}
+        processResponseHeaders(state, innerState, request);
+        InputStream entityStream = openResponseEntity(state, request);
+        mNotification.onDownloadStateChanged(IDownloaderClient.STATE_DOWNLOADING);
+        transferData(state, innerState, data, entityStream);
+    }
 
-	/**
+    /**
      * Check if current connectivity is valid for this request.
      */
-	private void checkConnectivity(State state) throws StopRequest {
-		switch (mService.getNetworkAvailabilityState(mDB)) {
-			case DownloaderService.NETWORK_OK:
-				return;
-			case DownloaderService.NETWORK_NO_CONNECTION:
-				throw new StopRequest(DownloaderService.STATUS_WAITING_FOR_NETWORK,
-						"waiting for network to return");
-			case DownloaderService.NETWORK_TYPE_DISALLOWED_BY_REQUESTOR:
-				throw new StopRequest(
-						DownloaderService.STATUS_QUEUED_FOR_WIFI_OR_CELLULAR_PERMISSION,
-						"waiting for wifi or for download over cellular to be authorized");
-			case DownloaderService.NETWORK_CANNOT_USE_ROAMING:
-				throw new StopRequest(DownloaderService.STATUS_WAITING_FOR_NETWORK,
-						"roaming is not allowed");
-			case DownloaderService.NETWORK_UNUSABLE_DUE_TO_SIZE:
-				throw new StopRequest(DownloaderService.STATUS_QUEUED_FOR_WIFI, "waiting for wifi");
-		}
-	}
-
-	/**
+    private void checkConnectivity(State state) throws StopRequest {
+        switch (mService.getNetworkAvailabilityState(mDB)) {
+            case DownloaderService.NETWORK_OK:
+                return;
+            case DownloaderService.NETWORK_NO_CONNECTION:
+                throw new StopRequest(DownloaderService.STATUS_WAITING_FOR_NETWORK,
+                        "waiting for network to return");
+            case DownloaderService.NETWORK_TYPE_DISALLOWED_BY_REQUESTOR:
+                throw new StopRequest(
+                        DownloaderService.STATUS_QUEUED_FOR_WIFI_OR_CELLULAR_PERMISSION,
+                        "waiting for wifi or for download over cellular to be authorized");
+            case DownloaderService.NETWORK_CANNOT_USE_ROAMING:
+                throw new StopRequest(DownloaderService.STATUS_WAITING_FOR_NETWORK,
+                        "roaming is not allowed");
+            case DownloaderService.NETWORK_UNUSABLE_DUE_TO_SIZE:
+                throw new StopRequest(DownloaderService.STATUS_QUEUED_FOR_WIFI, "waiting for wifi");
+        }
+    }
+
+    /**
      * Transfer as much data as possible from the HTTP response to the
      * destination file.
      *
      * @param data buffer to use to read data
      * @param entityStream stream for reading the HTTP response entity
      */
-	private void transferData(State state, InnerState innerState, byte[] data,
-			InputStream entityStream) throws StopRequest {
-		for (;;) {
-			int bytesRead = readFromResponse(state, innerState, data, entityStream);
-			if (bytesRead == -1) { // success, end of stream already reached
-				handleEndOfStream(state, innerState);
-				return;
-			}
-
-			state.mGotData = true;
-			writeDataToDestination(state, data, bytesRead);
-			innerState.mBytesSoFar += bytesRead;
-			innerState.mBytesThisSession += bytesRead;
-			reportProgress(state, innerState);
-
-			checkPausedOrCanceled(state);
-		}
-	}
-
-	/**
+    private void transferData(State state, InnerState innerState, byte[] data,
+            InputStream entityStream) throws StopRequest {
+        for (;;) {
+            int bytesRead = readFromResponse(state, innerState, data, entityStream);
+            if (bytesRead == -1) { // success, end of stream already reached
+                handleEndOfStream(state, innerState);
+                return;
+            }
+
+            state.mGotData = true;
+            writeDataToDestination(state, data, bytesRead);
+            innerState.mBytesSoFar += bytesRead;
+            innerState.mBytesThisSession += bytesRead;
+            reportProgress(state, innerState);
+
+            checkPausedOrCanceled(state);
+        }
+    }
+
+    /**
      * Called after a successful completion to take any necessary action on the
      * downloaded file.
      */
-	private void finalizeDestinationFile(State state) throws StopRequest {
-		syncDestination(state);
-		String tempFilename = state.mFilename;
-		String finalFilename = Helpers.generateSaveFileName(mService, mInfo.mFileName);
-		if (!state.mFilename.equals(finalFilename)) {
-			File startFile = new File(tempFilename);
-			File destFile = new File(finalFilename);
-			if (mInfo.mTotalBytes != -1 && mInfo.mCurrentBytes == mInfo.mTotalBytes) {
-				if (!startFile.renameTo(destFile)) {
-					throw new StopRequest(DownloaderService.STATUS_FILE_ERROR,
-							"unable to finalize destination file");
-				}
-			} else {
-				throw new StopRequest(DownloaderService.STATUS_FILE_DELIVERED_INCORRECTLY,
-						"file delivered with incorrect size. probably due to network not browser configured");
-			}
-		}
-	}
-
-	/**
+    private void finalizeDestinationFile(State state) throws StopRequest {
+        syncDestination(state);
+        String tempFilename = state.mFilename;
+        String finalFilename = Helpers.generateSaveFileName(mService, mInfo.mFileName);
+        if (!state.mFilename.equals(finalFilename)) {
+            File startFile = new File(tempFilename);
+            File destFile = new File(finalFilename);
+            if (mInfo.mTotalBytes != -1 && mInfo.mCurrentBytes == mInfo.mTotalBytes) {
+                if (!startFile.renameTo(destFile)) {
+                    throw new StopRequest(DownloaderService.STATUS_FILE_ERROR,
+                            "unable to finalize destination file");
+                }
+            } else {
+                throw new StopRequest(DownloaderService.STATUS_FILE_DELIVERED_INCORRECTLY,
+                        "file delivered with incorrect size. probably due to network not browser configured");
+            }
+        }
+    }
+
+    /**
      * Called just before the thread finishes, regardless of status, to take any
      * necessary action on the downloaded file.
      */
-	private void cleanupDestination(State state, int finalStatus) {
-		closeDestination(state);
-		if (state.mFilename != null && DownloaderService.isStatusError(finalStatus)) {
-			new File(state.mFilename).delete();
-			state.mFilename = null;
-		}
-	}
-
-	/**
+    private void cleanupDestination(State state, int finalStatus) {
+        closeDestination(state);
+        if (state.mFilename != null && DownloaderService.isStatusError(finalStatus)) {
+            new File(state.mFilename).delete();
+            state.mFilename = null;
+        }
+    }
+
+    /**
      * Sync the destination file to storage.
      */
-	private void syncDestination(State state) {
-		FileOutputStream downloadedFileStream = null;
-		try {
-			downloadedFileStream = new FileOutputStream(state.mFilename, true);
-			downloadedFileStream.getFD().sync();
-		} catch (FileNotFoundException ex) {
-			Log.w(Constants.TAG, "file " + state.mFilename + " not found: " + ex);
-		} catch (SyncFailedException ex) {
-			Log.w(Constants.TAG, "file " + state.mFilename + " sync failed: " + ex);
-		} catch (IOException ex) {
-			Log.w(Constants.TAG, "IOException trying to sync " + state.mFilename + ": " + ex);
-		} catch (RuntimeException ex) {
-			Log.w(Constants.TAG, "exception while syncing file: ", ex);
-		} finally {
-			if (downloadedFileStream != null) {
-				try {
-					downloadedFileStream.close();
-				} catch (IOException ex) {
-					Log.w(Constants.TAG, "IOException while closing synced file: ", ex);
-				} catch (RuntimeException ex) {
-					Log.w(Constants.TAG, "exception while closing file: ", ex);
-				}
-			}
-		}
-	}
-
-	/**
+    private void syncDestination(State state) {
+        FileOutputStream downloadedFileStream = null;
+        try {
+            downloadedFileStream = new FileOutputStream(state.mFilename, true);
+            downloadedFileStream.getFD().sync();
+        } catch (FileNotFoundException ex) {
+            Log.w(Constants.TAG, "file " + state.mFilename + " not found: " + ex);
+        } catch (SyncFailedException ex) {
+            Log.w(Constants.TAG, "file " + state.mFilename + " sync failed: " + ex);
+        } catch (IOException ex) {
+            Log.w(Constants.TAG, "IOException trying to sync " + state.mFilename + ": " + ex);
+        } catch (RuntimeException ex) {
+            Log.w(Constants.TAG, "exception while syncing file: ", ex);
+        } finally {
+            if (downloadedFileStream != null) {
+                try {
+                    downloadedFileStream.close();
+                } catch (IOException ex) {
+                    Log.w(Constants.TAG, "IOException while closing synced file: ", ex);
+                } catch (RuntimeException ex) {
+                    Log.w(Constants.TAG, "exception while closing file: ", ex);
+                }
+            }
+        }
+    }
+
+    /**
      * Close the destination output stream.
      */
-	private void closeDestination(State state) {
-		try {
-			// close the file
-			if (state.mStream != null) {
-				state.mStream.close();
-				state.mStream = null;
-			}
-		} catch (IOException ex) {
-			if (Constants.LOGV) {
-				Log.v(Constants.TAG, "exception when closing the file after download : " + ex);
-			}
-			// nothing can really be done if the file can't be closed
-		}
-	}
-
-	/**
+    private void closeDestination(State state) {
+        try {
+            // close the file
+            if (state.mStream != null) {
+                state.mStream.close();
+                state.mStream = null;
+            }
+        } catch (IOException ex) {
+            if (Constants.LOGV) {
+                Log.v(Constants.TAG, "exception when closing the file after download : " + ex);
+            }
+            // nothing can really be done if the file can't be closed
+        }
+    }
+
+    /**
      * Check if the download has been paused or canceled, stopping the request
      * appropriately if it has been.
      */
-	private void checkPausedOrCanceled(State state) throws StopRequest {
-		if (mService.getControl() == DownloaderService.CONTROL_PAUSED) {
-			int status = mService.getStatus();
-			switch (status) {
-				case DownloaderService.STATUS_PAUSED_BY_APP:
-					throw new StopRequest(mService.getStatus(),
-							"download paused");
-			}
-		}
-	}
-
-	/**
+    private void checkPausedOrCanceled(State state) throws StopRequest {
+        if (mService.getControl() == DownloaderService.CONTROL_PAUSED) {
+            int status = mService.getStatus();
+            switch (status) {
+                case DownloaderService.STATUS_PAUSED_BY_APP:
+                    throw new StopRequest(mService.getStatus(),
+                            "download paused");
+            }
+        }
+    }
+
+    /**
      * Report download progress through the database if necessary.
      */
-	private void reportProgress(State state, InnerState innerState) {
-		long now = System.currentTimeMillis();
-		if (innerState.mBytesSoFar - innerState.mBytesNotified > Constants.MIN_PROGRESS_STEP && now - innerState.mTimeLastNotification > Constants.MIN_PROGRESS_TIME) {
-			// we store progress updates to the database here
-			mInfo.mCurrentBytes = innerState.mBytesSoFar;
-			mDB.updateDownloadCurrentBytes(mInfo);
-
-			innerState.mBytesNotified = innerState.mBytesSoFar;
-			innerState.mTimeLastNotification = now;
-
-			long totalBytesSoFar = innerState.mBytesThisSession + mService.mBytesSoFar;
-
-			if (Constants.LOGVV) {
-				Log.v(Constants.TAG, "downloaded " + mInfo.mCurrentBytes + " out of " + mInfo.mTotalBytes);
-				Log.v(Constants.TAG, "     total " + totalBytesSoFar + " out of " + mService.mTotalLength);
-			}
-
-			mService.notifyUpdateBytes(totalBytesSoFar);
-		}
-	}
-
-	/**
+    private void reportProgress(State state, InnerState innerState) {
+        long now = System.currentTimeMillis();
+        if (innerState.mBytesSoFar - innerState.mBytesNotified
+                > Constants.MIN_PROGRESS_STEP
+                && now - innerState.mTimeLastNotification
+                > Constants.MIN_PROGRESS_TIME) {
+            // we store progress updates to the database here
+            mInfo.mCurrentBytes = innerState.mBytesSoFar;
+            mDB.updateDownloadCurrentBytes(mInfo);
+
+            innerState.mBytesNotified = innerState.mBytesSoFar;
+            innerState.mTimeLastNotification = now;
+
+            long totalBytesSoFar = innerState.mBytesThisSession + mService.mBytesSoFar;
+
+            if (Constants.LOGVV) {
+                Log.v(Constants.TAG, "downloaded " + mInfo.mCurrentBytes + " out of "
+                        + mInfo.mTotalBytes);
+                Log.v(Constants.TAG, "     total " + totalBytesSoFar + " out of "
+                        + mService.mTotalLength);
+            }
+
+            mService.notifyUpdateBytes(totalBytesSoFar);
+        }
+    }
+
+    /**
      * Write a data buffer to the destination file.
      *
      * @param data buffer containing the data to write
      * @param bytesRead how many bytes to write from the buffer
      */
-	private void writeDataToDestination(State state, byte[] data, int bytesRead)
-			throws StopRequest {
-		for (;;) {
-			try {
-				if (state.mStream == null) {
-					state.mStream = new FileOutputStream(state.mFilename, true);
-				}
-				state.mStream.write(data, 0, bytesRead);
-				// we close after every write --- this may be too inefficient
-				closeDestination(state);
-				return;
-			} catch (IOException ex) {
-				if (!Helpers.isExternalMediaMounted()) {
-					throw new StopRequest(DownloaderService.STATUS_DEVICE_NOT_FOUND_ERROR,
-							"external media not mounted while writing destination file");
-				}
-
-				long availableBytes =
-						Helpers.getAvailableBytes(Helpers.getFilesystemRoot(state.mFilename));
-				if (availableBytes < bytesRead) {
-					throw new StopRequest(DownloaderService.STATUS_INSUFFICIENT_SPACE_ERROR,
-							"insufficient space while writing destination file", ex);
-				}
-				throw new StopRequest(DownloaderService.STATUS_FILE_ERROR,
-						"while writing destination file: " + ex.toString(), ex);
-			}
-		}
-	}
-
-	/**
+    private void writeDataToDestination(State state, byte[] data, int bytesRead)
+            throws StopRequest {
+        for (;;) {
+            try {
+                if (state.mStream == null) {
+                    state.mStream = new FileOutputStream(state.mFilename, true);
+                }
+                state.mStream.write(data, 0, bytesRead);
+                // we close after every write --- this may be too inefficient
+                closeDestination(state);
+                return;
+            } catch (IOException ex) {
+                if (!Helpers.isExternalMediaMounted()) {
+                    throw new StopRequest(DownloaderService.STATUS_DEVICE_NOT_FOUND_ERROR,
+                            "external media not mounted while writing destination file");
+                }
+
+                long availableBytes =
+                        Helpers.getAvailableBytes(Helpers.getFilesystemRoot(state.mFilename));
+                if (availableBytes < bytesRead) {
+                    throw new StopRequest(DownloaderService.STATUS_INSUFFICIENT_SPACE_ERROR,
+                            "insufficient space while writing destination file", ex);
+                }
+                throw new StopRequest(DownloaderService.STATUS_FILE_ERROR,
+                        "while writing destination file: " + ex.toString(), ex);
+            }
+        }
+    }
+
+    /**
      * Called when we've reached the end of the HTTP response stream, to update
      * the database and check for consistency.
      */
-	private void handleEndOfStream(State state, InnerState innerState) throws StopRequest {
-		mInfo.mCurrentBytes = innerState.mBytesSoFar;
-		// this should always be set from the market
-		// if ( innerState.mHeaderContentLength == null ) {
-		// mInfo.mTotalBytes = innerState.mBytesSoFar;
-		// }
-		mDB.updateDownload(mInfo);
-
-		boolean lengthMismatched = (innerState.mHeaderContentLength != null) && (innerState.mBytesSoFar != Integer.parseInt(innerState.mHeaderContentLength));
-		if (lengthMismatched) {
-			if (cannotResume(innerState)) {
-				throw new StopRequest(DownloaderService.STATUS_CANNOT_RESUME,
-						"mismatched content length");
-			} else {
-				throw new StopRequest(getFinalStatusForHttpError(state),
-						"closed socket before end of file");
-			}
-		}
-	}
-
-	private boolean cannotResume(InnerState innerState) {
-		return innerState.mBytesSoFar > 0 && innerState.mHeaderETag == null;
-	}
-
-	/**
+    private void handleEndOfStream(State state, InnerState innerState) throws StopRequest {
+        mInfo.mCurrentBytes = innerState.mBytesSoFar;
+        // this should always be set from the market
+        // if ( innerState.mHeaderContentLength == null ) {
+        // mInfo.mTotalBytes = innerState.mBytesSoFar;
+        // }
+        mDB.updateDownload(mInfo);
+
+        boolean lengthMismatched = (innerState.mHeaderContentLength != null)
+                && (innerState.mBytesSoFar != Integer.parseInt(innerState.mHeaderContentLength));
+        if (lengthMismatched) {
+            if (cannotResume(innerState)) {
+                throw new StopRequest(DownloaderService.STATUS_CANNOT_RESUME,
+                        "mismatched content length");
+            } else {
+                throw new StopRequest(getFinalStatusForHttpError(state),
+                        "closed socket before end of file");
+            }
+        }
+    }
+
+    private boolean cannotResume(InnerState innerState) {
+        return innerState.mBytesSoFar > 0 && innerState.mHeaderETag == null;
+    }
+
+    /**
      * Read some data from the HTTP response stream, handling I/O errors.
      *
      * @param data buffer to use to read data
@@ -476,358 +488,365 @@ public class DownloadThread {
      * @return the number of bytes actually read or -1 if the end of the stream
      *         has been reached
      */
-	private int readFromResponse(State state, InnerState innerState, byte[] data,
-			InputStream entityStream) throws StopRequest {
-		try {
-			return entityStream.read(data);
-		} catch (IOException ex) {
-			logNetworkState();
-			mInfo.mCurrentBytes = innerState.mBytesSoFar;
-			mDB.updateDownload(mInfo);
-			if (cannotResume(innerState)) {
-				String message = "while reading response: " + ex.toString() + ", can't resume interrupted download with no ETag";
-				throw new StopRequest(DownloaderService.STATUS_CANNOT_RESUME,
-						message, ex);
-			} else {
-				throw new StopRequest(getFinalStatusForHttpError(state),
-						"while reading response: " + ex.toString(), ex);
-			}
-		}
-	}
-
-	/**
+    private int readFromResponse(State state, InnerState innerState, byte[] data,
+            InputStream entityStream) throws StopRequest {
+        try {
+            return entityStream.read(data);
+        } catch (IOException ex) {
+            logNetworkState();
+            mInfo.mCurrentBytes = innerState.mBytesSoFar;
+            mDB.updateDownload(mInfo);
+            if (cannotResume(innerState)) {
+                String message = "while reading response: " + ex.toString()
+                        + ", can't resume interrupted download with no ETag";
+                throw new StopRequest(DownloaderService.STATUS_CANNOT_RESUME,
+                        message, ex);
+            } else {
+                throw new StopRequest(getFinalStatusForHttpError(state),
+                        "while reading response: " + ex.toString(), ex);
+            }
+        }
+    }
+
+    /**
      * Open a stream for the HTTP response entity, handling I/O errors.
      *
      * @return an InputStream to read the response entity
      */
-	private InputStream openResponseEntity(State state, HttpURLConnection response)
-			throws StopRequest {
-		try {
-			return response.getInputStream();
-		} catch (IOException ex) {
-			logNetworkState();
-			throw new StopRequest(getFinalStatusForHttpError(state),
-					"while getting entity: " + ex.toString(), ex);
-		}
-	}
-
-	private void logNetworkState() {
-		if (Constants.LOGX) {
-			Log.i(Constants.TAG,
-					"Net " + (mService.getNetworkAvailabilityState(mDB) == DownloaderService.NETWORK_OK ? "Up" : "Down"));
-		}
-	}
-
-	/**
+    private InputStream openResponseEntity(State state, HttpURLConnection response)
+            throws StopRequest {
+        try {
+            return response.getInputStream();
+        } catch (IOException ex) {
+            logNetworkState();
+            throw new StopRequest(getFinalStatusForHttpError(state),
+                    "while getting entity: " + ex.toString(), ex);
+        }
+    }
+
+    private void logNetworkState() {
+        if (Constants.LOGX) {
+            Log.i(Constants.TAG,
+                    "Net "
+                            + (mService.getNetworkAvailabilityState(mDB) == DownloaderService.NETWORK_OK ? "Up"
+                                    : "Down"));
+        }
+    }
+
+    /**
      * Read HTTP response headers and take appropriate action, including setting
      * up the destination file and updating the database.
      */
-	private void processResponseHeaders(State state, InnerState innerState, HttpURLConnection response)
-			throws StopRequest {
-		if (innerState.mContinuingDownload) {
-			// ignore response headers on resume requests
-			return;
-		}
-
-		readResponseHeaders(state, innerState, response);
-
-		try {
-			state.mFilename = mService.generateSaveFile(mInfo.mFileName, mInfo.mTotalBytes);
-		} catch (DownloaderService.GenerateSaveFileError exc) {
-			throw new StopRequest(exc.mStatus, exc.mMessage);
-		}
-		try {
-			state.mStream = new FileOutputStream(state.mFilename);
-		} catch (FileNotFoundException exc) {
-			// make sure the directory exists
-			File pathFile = new File(Helpers.getSaveFilePath(mService));
-			try {
-				if (pathFile.mkdirs()) {
-					state.mStream = new FileOutputStream(state.mFilename);
-				}
-			} catch (Exception ex) {
-				throw new StopRequest(DownloaderService.STATUS_FILE_ERROR,
-						"while opening destination file: " + exc.toString(), exc);
-			}
-		}
-		if (Constants.LOGV) {
-			Log.v(Constants.TAG, "writing " + mInfo.mUri + " to " + state.mFilename);
-		}
-
-		updateDatabaseFromHeaders(state, innerState);
-		// check connectivity again now that we know the total size
-		checkConnectivity(state);
-	}
-
-	/**
+    private void processResponseHeaders(State state, InnerState innerState, HttpURLConnection response)
+            throws StopRequest {
+        if (innerState.mContinuingDownload) {
+            // ignore response headers on resume requests
+            return;
+        }
+
+        readResponseHeaders(state, innerState, response);
+
+        try {
+            state.mFilename = mService.generateSaveFile(mInfo.mFileName, mInfo.mTotalBytes);
+        } catch (DownloaderService.GenerateSaveFileError exc) {
+            throw new StopRequest(exc.mStatus, exc.mMessage);
+        }
+        try {
+            state.mStream = new FileOutputStream(state.mFilename);
+        } catch (FileNotFoundException exc) {
+            // make sure the directory exists
+            File pathFile = new File(Helpers.getSaveFilePath(mService));
+            try {
+                if (pathFile.mkdirs()) {
+                    state.mStream = new FileOutputStream(state.mFilename);
+                }
+            } catch (Exception ex) {
+                throw new StopRequest(DownloaderService.STATUS_FILE_ERROR,
+                        "while opening destination file: " + exc.toString(), exc);
+            }
+        }
+        if (Constants.LOGV) {
+            Log.v(Constants.TAG, "writing " + mInfo.mUri + " to " + state.mFilename);
+        }
+
+        updateDatabaseFromHeaders(state, innerState);
+        // check connectivity again now that we know the total size
+        checkConnectivity(state);
+    }
+
+    /**
      * Update necessary database fields based on values of HTTP response headers
      * that have been read.
      */
-	private void updateDatabaseFromHeaders(State state, InnerState innerState) {
-		mInfo.mETag = innerState.mHeaderETag;
-		mDB.updateDownload(mInfo);
-	}
+    private void updateDatabaseFromHeaders(State state, InnerState innerState) {
+        mInfo.mETag = innerState.mHeaderETag;
+        mDB.updateDownload(mInfo);
+    }
 
-	/**
+    /**
      * Read headers from the HTTP response and store them into local state.
      */
-	private void readResponseHeaders(State state, InnerState innerState, HttpURLConnection response)
-			throws StopRequest {
-		String value = response.getHeaderField("Content-Disposition");
-		if (value != null) {
-			innerState.mHeaderContentDisposition = value;
-		}
-		value = response.getHeaderField("Content-Location");
-		if (value != null) {
-			innerState.mHeaderContentLocation = value;
-		}
-		value = response.getHeaderField("ETag");
-		if (value != null) {
-			innerState.mHeaderETag = value;
-		}
-		String headerTransferEncoding = null;
-		value = response.getHeaderField("Transfer-Encoding");
-		if (value != null) {
-			headerTransferEncoding = value;
-		}
-		String headerContentType = null;
-		value = response.getHeaderField("Content-Type");
-		if (value != null) {
-			headerContentType = value;
-			if (!headerContentType.equals("application/vnd.android.obb")) {
-				throw new StopRequest(DownloaderService.STATUS_FILE_DELIVERED_INCORRECTLY,
-						"file delivered with incorrect Mime type");
-			}
-		}
-
-		if (headerTransferEncoding == null) {
-			long contentLength = response.getContentLength();
-			if (value != null) {
-				// this is always set from Market
-				if (contentLength != -1 && contentLength != mInfo.mTotalBytes) {
-					// we're most likely on a bad wifi connection -- we should
-					// probably
-					// also look at the mime type --- but the size mismatch is
-					// enough
-					// to tell us that something is wrong here
-					Log.e(Constants.TAG, "Incorrect file size delivered.");
-				} else {
-					innerState.mHeaderContentLength = Long.toString(contentLength);
-				}
-			}
-		} else {
-			// Ignore content-length with transfer-encoding - 2616 4.4 3
-			if (Constants.LOGVV) {
-				Log.v(Constants.TAG,
-						"ignoring content-length because of xfer-encoding");
-			}
-		}
-		if (Constants.LOGVV) {
-			Log.v(Constants.TAG, "Content-Disposition: " +
-										 innerState.mHeaderContentDisposition);
-			Log.v(Constants.TAG, "Content-Length: " + innerState.mHeaderContentLength);
-			Log.v(Constants.TAG, "Content-Location: " + innerState.mHeaderContentLocation);
-			Log.v(Constants.TAG, "ETag: " + innerState.mHeaderETag);
-			Log.v(Constants.TAG, "Transfer-Encoding: " + headerTransferEncoding);
-		}
-
-		boolean noSizeInfo = innerState.mHeaderContentLength == null && (headerTransferEncoding == null || !headerTransferEncoding.equalsIgnoreCase("chunked"));
-		if (noSizeInfo) {
-			throw new StopRequest(DownloaderService.STATUS_HTTP_DATA_ERROR,
-					"can't know size of download, giving up");
-		}
-	}
-
-	/**
+    private void readResponseHeaders(State state, InnerState innerState, HttpURLConnection response)
+            throws StopRequest {
+        String value = response.getHeaderField("Content-Disposition");
+        if (value != null) {
+            innerState.mHeaderContentDisposition = value;
+        }
+        value = response.getHeaderField("Content-Location");
+        if (value != null) {
+            innerState.mHeaderContentLocation = value;
+        }
+        value = response.getHeaderField("ETag");
+        if (value != null) {
+            innerState.mHeaderETag = value;
+        }
+        String headerTransferEncoding = null;
+        value = response.getHeaderField("Transfer-Encoding");
+        if (value != null) {
+            headerTransferEncoding = value;
+        }
+        String headerContentType = null;
+        value = response.getHeaderField("Content-Type");
+        if (value != null) {
+            headerContentType = value;
+            if (!headerContentType.equals("application/vnd.android.obb")) {
+                throw new StopRequest(DownloaderService.STATUS_FILE_DELIVERED_INCORRECTLY,
+                        "file delivered with incorrect Mime type");
+            }
+        }
+
+        if (headerTransferEncoding == null) {
+            long contentLength = response.getContentLength();
+            if (value != null) {
+                // this is always set from Market
+                if (contentLength != -1 && contentLength != mInfo.mTotalBytes) {
+                    // we're most likely on a bad wifi connection -- we should
+                    // probably
+                    // also look at the mime type --- but the size mismatch is
+                    // enough
+                    // to tell us that something is wrong here
+                    Log.e(Constants.TAG, "Incorrect file size delivered.");
+                } else {
+                    innerState.mHeaderContentLength = Long.toString(contentLength);
+                }
+            }
+        } else {
+            // Ignore content-length with transfer-encoding - 2616 4.4 3
+            if (Constants.LOGVV) {
+                Log.v(Constants.TAG,
+                        "ignoring content-length because of xfer-encoding");
+            }
+        }
+        if (Constants.LOGVV) {
+            Log.v(Constants.TAG, "Content-Disposition: " +
+                    innerState.mHeaderContentDisposition);
+            Log.v(Constants.TAG, "Content-Length: " + innerState.mHeaderContentLength);
+            Log.v(Constants.TAG, "Content-Location: " + innerState.mHeaderContentLocation);
+            Log.v(Constants.TAG, "ETag: " + innerState.mHeaderETag);
+            Log.v(Constants.TAG, "Transfer-Encoding: " + headerTransferEncoding);
+        }
+
+        boolean noSizeInfo = innerState.mHeaderContentLength == null
+                && (headerTransferEncoding == null
+                || !headerTransferEncoding.equalsIgnoreCase("chunked"));
+        if (noSizeInfo) {
+            throw new StopRequest(DownloaderService.STATUS_HTTP_DATA_ERROR,
+                    "can't know size of download, giving up");
+        }
+    }
+
+    /**
      * Check the HTTP response status and handle anything unusual (e.g. not
      * 200/206).
      */
-	private void handleExceptionalStatus(State state, InnerState innerState, HttpURLConnection connection, int responseCode)
-			throws StopRequest, RetryDownload {
-		if (responseCode == 503 && mInfo.mNumFailed < Constants.MAX_RETRIES) {
-			handleServiceUnavailable(state, connection);
-		}
-		int expectedStatus = innerState.mContinuingDownload ? 206 : DownloaderService.STATUS_SUCCESS;
-		if (responseCode != expectedStatus) {
-			handleOtherStatus(state, innerState, responseCode);
-		} else {
-			// no longer redirected
-			state.mRedirectCount = 0;
-		}
-	}
-
-	/**
+    private void handleExceptionalStatus(State state, InnerState innerState, HttpURLConnection connection, int responseCode)
+            throws StopRequest, RetryDownload {
+        if (responseCode == 503 && mInfo.mNumFailed < Constants.MAX_RETRIES) {
+            handleServiceUnavailable(state, connection);
+        }
+        int expectedStatus = innerState.mContinuingDownload ? 206
+                : DownloaderService.STATUS_SUCCESS;
+        if (responseCode != expectedStatus) {
+            handleOtherStatus(state, innerState, responseCode);
+        } else {
+            // no longer redirected
+            state.mRedirectCount = 0;
+        }
+    }
+
+    /**
      * Handle a status that we don't know how to deal with properly.
      */
-	private void handleOtherStatus(State state, InnerState innerState, int statusCode)
-			throws StopRequest {
-		int finalStatus;
-		if (DownloaderService.isStatusError(statusCode)) {
-			finalStatus = statusCode;
-		} else if (statusCode >= 300 && statusCode < 400) {
-			finalStatus = DownloaderService.STATUS_UNHANDLED_REDIRECT;
-		} else if (innerState.mContinuingDownload && statusCode == DownloaderService.STATUS_SUCCESS) {
-			finalStatus = DownloaderService.STATUS_CANNOT_RESUME;
-		} else {
-			finalStatus = DownloaderService.STATUS_UNHANDLED_HTTP_CODE;
-		}
-		throw new StopRequest(finalStatus, "http error " + statusCode);
-	}
-
-	/**
+    private void handleOtherStatus(State state, InnerState innerState, int statusCode)
+            throws StopRequest {
+        int finalStatus;
+        if (DownloaderService.isStatusError(statusCode)) {
+            finalStatus = statusCode;
+        } else if (statusCode >= 300 && statusCode < 400) {
+            finalStatus = DownloaderService.STATUS_UNHANDLED_REDIRECT;
+        } else if (innerState.mContinuingDownload && statusCode == DownloaderService.STATUS_SUCCESS) {
+            finalStatus = DownloaderService.STATUS_CANNOT_RESUME;
+        } else {
+            finalStatus = DownloaderService.STATUS_UNHANDLED_HTTP_CODE;
+        }
+        throw new StopRequest(finalStatus, "http error " + statusCode);
+    }
+
+    /**
      * Add headers for this download to the HTTP request to allow for resume.
      */
-	private void addRequestHeaders(InnerState innerState, HttpURLConnection request) {
-		if (innerState.mContinuingDownload) {
-			if (innerState.mHeaderETag != null) {
-				request.setRequestProperty("If-Match", innerState.mHeaderETag);
-			}
-			request.setRequestProperty("Range", "bytes=" + innerState.mBytesSoFar + "-");
-		}
-	}
-
-	/**
+    private void addRequestHeaders(InnerState innerState, HttpURLConnection request) {
+        if (innerState.mContinuingDownload) {
+            if (innerState.mHeaderETag != null) {
+                request.setRequestProperty("If-Match", innerState.mHeaderETag);
+            }
+            request.setRequestProperty("Range", "bytes=" + innerState.mBytesSoFar + "-");
+        }
+    }
+
+    /**
      * Handle a 503 Service Unavailable status by processing the Retry-After
      * header.
      */
-	private void handleServiceUnavailable(State state, HttpURLConnection connection) throws StopRequest {
-		if (Constants.LOGVV) {
-			Log.v(Constants.TAG, "got HTTP response code 503");
-		}
-		state.mCountRetry = true;
-		String retryAfterValue = connection.getHeaderField("Retry-After");
-		if (retryAfterValue != null) {
-			try {
-				if (Constants.LOGVV) {
-					Log.v(Constants.TAG, "Retry-After :" + retryAfterValue);
-				}
-				state.mRetryAfter = Integer.parseInt(retryAfterValue);
-				if (state.mRetryAfter < 0) {
-					state.mRetryAfter = 0;
-				} else {
-					if (state.mRetryAfter < Constants.MIN_RETRY_AFTER) {
-						state.mRetryAfter = Constants.MIN_RETRY_AFTER;
-					} else if (state.mRetryAfter > Constants.MAX_RETRY_AFTER) {
-						state.mRetryAfter = Constants.MAX_RETRY_AFTER;
-					}
-					state.mRetryAfter += Helpers.sRandom.nextInt(Constants.MIN_RETRY_AFTER + 1);
-					state.mRetryAfter *= 1000;
-				}
-			} catch (NumberFormatException ex) {
-				// ignored - retryAfter stays 0 in this case.
-			}
-		}
-		throw new StopRequest(DownloaderService.STATUS_WAITING_TO_RETRY,
-				"got 503 Service Unavailable, will retry later");
-	}
-
-	/**
+    private void handleServiceUnavailable(State state, HttpURLConnection connection) throws StopRequest {
+        if (Constants.LOGVV) {
+            Log.v(Constants.TAG, "got HTTP response code 503");
+        }
+        state.mCountRetry = true;
+        String retryAfterValue = connection.getHeaderField("Retry-After");
+        if (retryAfterValue != null) {
+            try {
+                if (Constants.LOGVV) {
+                    Log.v(Constants.TAG, "Retry-After :" + retryAfterValue);
+                }
+                state.mRetryAfter = Integer.parseInt(retryAfterValue);
+                if (state.mRetryAfter < 0) {
+                    state.mRetryAfter = 0;
+                } else {
+                    if (state.mRetryAfter < Constants.MIN_RETRY_AFTER) {
+                        state.mRetryAfter = Constants.MIN_RETRY_AFTER;
+                    } else if (state.mRetryAfter > Constants.MAX_RETRY_AFTER) {
+                        state.mRetryAfter = Constants.MAX_RETRY_AFTER;
+                    }
+                    state.mRetryAfter += Helpers.sRandom.nextInt(Constants.MIN_RETRY_AFTER + 1);
+                    state.mRetryAfter *= 1000;
+                }
+            } catch (NumberFormatException ex) {
+                // ignored - retryAfter stays 0 in this case.
+            }
+        }
+        throw new StopRequest(DownloaderService.STATUS_WAITING_TO_RETRY,
+                "got 503 Service Unavailable, will retry later");
+    }
+
+    /**
      * Send the request to the server, handling any I/O exceptions.
      */
-	private int sendRequest(State state, HttpURLConnection request)
-			throws StopRequest {
-		try {
-			return request.getResponseCode();
-		} catch (IllegalArgumentException ex) {
-			throw new StopRequest(DownloaderService.STATUS_HTTP_DATA_ERROR,
-					"while trying to execute request: " + ex.toString(), ex);
-		} catch (IOException ex) {
-			logNetworkState();
-			throw new StopRequest(getFinalStatusForHttpError(state),
-					"while trying to execute request: " + ex.toString(), ex);
-		}
-	}
-
-	private int getFinalStatusForHttpError(State state) {
-		if (mService.getNetworkAvailabilityState(mDB) != DownloaderService.NETWORK_OK) {
-			return DownloaderService.STATUS_WAITING_FOR_NETWORK;
-		} else if (mInfo.mNumFailed < Constants.MAX_RETRIES) {
-			state.mCountRetry = true;
-			return DownloaderService.STATUS_WAITING_TO_RETRY;
-		} else {
-			Log.w(Constants.TAG, "reached max retries for " + mInfo.mNumFailed);
-			return DownloaderService.STATUS_HTTP_DATA_ERROR;
-		}
-	}
-
-	/**
+    private int sendRequest(State state, HttpURLConnection request)
+            throws StopRequest {
+        try {
+            return request.getResponseCode();
+        } catch (IllegalArgumentException ex) {
+            throw new StopRequest(DownloaderService.STATUS_HTTP_DATA_ERROR,
+                    "while trying to execute request: " + ex.toString(), ex);
+        } catch (IOException ex) {
+            logNetworkState();
+            throw new StopRequest(getFinalStatusForHttpError(state),
+                    "while trying to execute request: " + ex.toString(), ex);
+        }
+    }
+
+    private int getFinalStatusForHttpError(State state) {
+        if (mService.getNetworkAvailabilityState(mDB) != DownloaderService.NETWORK_OK) {
+            return DownloaderService.STATUS_WAITING_FOR_NETWORK;
+        } else if (mInfo.mNumFailed < Constants.MAX_RETRIES) {
+            state.mCountRetry = true;
+            return DownloaderService.STATUS_WAITING_TO_RETRY;
+        } else {
+            Log.w(Constants.TAG, "reached max retries for " + mInfo.mNumFailed);
+            return DownloaderService.STATUS_HTTP_DATA_ERROR;
+        }
+    }
+
+    /**
      * Prepare the destination file to receive data. If the file already exists,
      * we'll set up appropriately for resumption.
      */
-	private void setupDestinationFile(State state, InnerState innerState)
-			throws StopRequest {
-		if (state.mFilename != null) { // only true if we've already run a
-			// thread for this download
-			if (!Helpers.isFilenameValid(state.mFilename)) {
-				// this should never happen
-				throw new StopRequest(DownloaderService.STATUS_FILE_ERROR,
-						"found invalid internal destination filename");
-			}
-			// We're resuming a download that got interrupted
-			File f = new File(state.mFilename);
-			if (f.exists()) {
-				long fileLength = f.length();
-				if (fileLength == 0) {
-					// The download hadn't actually started, we can restart from
-					// scratch
-					f.delete();
-					state.mFilename = null;
-				} else if (mInfo.mETag == null) {
-					// This should've been caught upon failure
-					f.delete();
-					throw new StopRequest(DownloaderService.STATUS_CANNOT_RESUME,
-							"Trying to resume a download that can't be resumed");
-				} else {
-					// All right, we'll be able to resume this download
-					try {
-						state.mStream = new FileOutputStream(state.mFilename, true);
-					} catch (FileNotFoundException exc) {
-						throw new StopRequest(DownloaderService.STATUS_FILE_ERROR,
-								"while opening destination for resuming: " + exc.toString(), exc);
-					}
-					innerState.mBytesSoFar = (int)fileLength;
-					if (mInfo.mTotalBytes != -1) {
-						innerState.mHeaderContentLength = Long.toString(mInfo.mTotalBytes);
-					}
-					innerState.mHeaderETag = mInfo.mETag;
-					innerState.mContinuingDownload = true;
-				}
-			}
-		}
-
-		if (state.mStream != null) {
-			closeDestination(state);
-		}
-	}
-
-	/**
+    private void setupDestinationFile(State state, InnerState innerState)
+            throws StopRequest {
+        if (state.mFilename != null) { // only true if we've already run a
+                                       // thread for this download
+            if (!Helpers.isFilenameValid(state.mFilename)) {
+                // this should never happen
+                throw new StopRequest(DownloaderService.STATUS_FILE_ERROR,
+                        "found invalid internal destination filename");
+            }
+            // We're resuming a download that got interrupted
+            File f = new File(state.mFilename);
+            if (f.exists()) {
+                long fileLength = f.length();
+                if (fileLength == 0) {
+                    // The download hadn't actually started, we can restart from
+                    // scratch
+                    f.delete();
+                    state.mFilename = null;
+                } else if (mInfo.mETag == null) {
+                    // This should've been caught upon failure
+                    f.delete();
+                    throw new StopRequest(DownloaderService.STATUS_CANNOT_RESUME,
+                            "Trying to resume a download that can't be resumed");
+                } else {
+                    // All right, we'll be able to resume this download
+                    try {
+                        state.mStream = new FileOutputStream(state.mFilename, true);
+                    } catch (FileNotFoundException exc) {
+                        throw new StopRequest(DownloaderService.STATUS_FILE_ERROR,
+                                "while opening destination for resuming: " + exc.toString(), exc);
+                    }
+                    innerState.mBytesSoFar = (int) fileLength;
+                    if (mInfo.mTotalBytes != -1) {
+                        innerState.mHeaderContentLength = Long.toString(mInfo.mTotalBytes);
+                    }
+                    innerState.mHeaderETag = mInfo.mETag;
+                    innerState.mContinuingDownload = true;
+                }
+            }
+        }
+
+        if (state.mStream != null) {
+            closeDestination(state);
+        }
+    }
+
+    /**
      * Stores information about the completed download, and notifies the
      * initiating application.
      */
-	private void notifyDownloadCompleted(
-			int status, boolean countRetry, int retryAfter, int redirectCount, boolean gotData,
-			String filename) {
-		updateDownloadDatabase(
-				status, countRetry, retryAfter, redirectCount, gotData, filename);
-		if (DownloaderService.isStatusCompleted(status)) {
-			// TBD: send status update?
-		}
-	}
-
-	private void updateDownloadDatabase(
-			int status, boolean countRetry, int retryAfter, int redirectCount, boolean gotData,
-			String filename) {
-		mInfo.mStatus = status;
-		mInfo.mRetryAfter = retryAfter;
-		mInfo.mRedirectCount = redirectCount;
-		mInfo.mLastMod = System.currentTimeMillis();
-		if (!countRetry) {
-			mInfo.mNumFailed = 0;
-		} else if (gotData) {
-			mInfo.mNumFailed = 1;
-		} else {
-			mInfo.mNumFailed++;
-		}
-		mDB.updateDownload(mInfo);
-	}
+    private void notifyDownloadCompleted(
+            int status, boolean countRetry, int retryAfter, int redirectCount, boolean gotData,
+            String filename) {
+        updateDownloadDatabase(
+                status, countRetry, retryAfter, redirectCount, gotData, filename);
+        if (DownloaderService.isStatusCompleted(status)) {
+            // TBD: send status update?
+        }
+    }
+
+    private void updateDownloadDatabase(
+            int status, boolean countRetry, int retryAfter, int redirectCount, boolean gotData,
+            String filename) {
+        mInfo.mStatus = status;
+        mInfo.mRetryAfter = retryAfter;
+        mInfo.mRedirectCount = redirectCount;
+        mInfo.mLastMod = System.currentTimeMillis();
+        if (!countRetry) {
+            mInfo.mNumFailed = 0;
+        } else if (gotData) {
+            mInfo.mNumFailed = 1;
+        } else {
+            mInfo.mNumFailed++;
+        }
+        mDB.updateDownload(mInfo);
+    }
+
 }

File diff ditekan karena terlalu besar
+ 578 - 561
platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloaderService.java


+ 463 - 422
platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadsDB.java

@@ -27,443 +27,484 @@ import android.provider.BaseColumns;
 import android.util.Log;
 
 public class DownloadsDB {
-	private static final String DATABASE_NAME = "DownloadsDB";
-	private static final int DATABASE_VERSION = 7;
-	public static final String LOG_TAG = DownloadsDB.class.getName();
-	final SQLiteOpenHelper mHelper;
-	SQLiteStatement mGetDownloadByIndex;
-	SQLiteStatement mUpdateCurrentBytes;
-	private static DownloadsDB mDownloadsDB;
-	long mMetadataRowID = -1;
-	int mVersionCode = -1;
-	int mStatus = -1;
-	int mFlags;
-
-	static public synchronized DownloadsDB getDB(Context paramContext) {
-		if (null == mDownloadsDB) {
-			return new DownloadsDB(paramContext);
-		}
-		return mDownloadsDB;
-	}
-
-	private SQLiteStatement getDownloadByIndexStatement() {
-		if (null == mGetDownloadByIndex) {
-			mGetDownloadByIndex = mHelper.getReadableDatabase().compileStatement(
-					"SELECT " + BaseColumns._ID + " FROM " + DownloadColumns.TABLE_NAME + " WHERE " + DownloadColumns.INDEX + " = ?");
-		}
-		return mGetDownloadByIndex;
-	}
-
-	private SQLiteStatement getUpdateCurrentBytesStatement() {
-		if (null == mUpdateCurrentBytes) {
-			mUpdateCurrentBytes = mHelper.getReadableDatabase().compileStatement(
-					"UPDATE " + DownloadColumns.TABLE_NAME + " SET " + DownloadColumns.CURRENTBYTES + " = ?"
-					+
-					" WHERE " + DownloadColumns.INDEX + " = ?");
-		}
-		return mUpdateCurrentBytes;
-	}
-
-	private DownloadsDB(Context paramContext) {
-		this.mHelper = new DownloadsContentDBHelper(paramContext);
-		final SQLiteDatabase sqldb = mHelper.getReadableDatabase();
-		// Query for the version code, the row ID of the metadata (for future
-		// updating) the status and the flags
-		Cursor cur = sqldb.rawQuery("SELECT " +
-											MetadataColumns.APKVERSION + "," +
-											BaseColumns._ID + "," +
-											MetadataColumns.DOWNLOAD_STATUS + "," +
-											MetadataColumns.FLAGS +
-											" FROM " + MetadataColumns.TABLE_NAME + " LIMIT 1",
-				null);
-		if (null != cur && cur.moveToFirst()) {
-			mVersionCode = cur.getInt(0);
-			mMetadataRowID = cur.getLong(1);
-			mStatus = cur.getInt(2);
-			mFlags = cur.getInt(3);
-			cur.close();
-		}
-		mDownloadsDB = this;
-	}
-
-	protected DownloadInfo getDownloadInfoByFileName(String fileName) {
-		final SQLiteDatabase sqldb = mHelper.getReadableDatabase();
-		Cursor itemcur = null;
-		try {
-			itemcur = sqldb.query(DownloadColumns.TABLE_NAME, DC_PROJECTION,
-					DownloadColumns.FILENAME + " = ?",
-					new String[] {
-							fileName },
-					null, null, null);
-			if (null != itemcur && itemcur.moveToFirst()) {
-				return getDownloadInfoFromCursor(itemcur);
-			}
-		} finally {
-			if (null != itemcur)
-				itemcur.close();
-		}
-		return null;
-	}
-
-	public long getIDForDownloadInfo(final DownloadInfo di) {
-		return getIDByIndex(di.mIndex);
-	}
-
-	public long getIDByIndex(int index) {
-		SQLiteStatement downloadByIndex = getDownloadByIndexStatement();
-		downloadByIndex.clearBindings();
-		downloadByIndex.bindLong(1, index);
-		try {
-			return downloadByIndex.simpleQueryForLong();
-		} catch (SQLiteDoneException e) {
-			return -1;
-		}
-	}
-
-	public void updateDownloadCurrentBytes(final DownloadInfo di) {
-		SQLiteStatement downloadCurrentBytes = getUpdateCurrentBytesStatement();
-		downloadCurrentBytes.clearBindings();
-		downloadCurrentBytes.bindLong(1, di.mCurrentBytes);
-		downloadCurrentBytes.bindLong(2, di.mIndex);
-		downloadCurrentBytes.execute();
-	}
-
-	public void close() {
-		this.mHelper.close();
-	}
-
-	protected static class DownloadsContentDBHelper extends SQLiteOpenHelper {
-		DownloadsContentDBHelper(Context paramContext) {
-			super(paramContext, DATABASE_NAME, null, DATABASE_VERSION);
-		}
-
-		private String createTableQueryFromArray(String paramString,
-				String[][] paramArrayOfString) {
-			StringBuilder localStringBuilder = new StringBuilder();
-			localStringBuilder.append("CREATE TABLE ");
-			localStringBuilder.append(paramString);
-			localStringBuilder.append(" (");
-			int i = paramArrayOfString.length;
-			for (int j = 0;; j++) {
-				if (j >= i) {
-					localStringBuilder
-							.setLength(localStringBuilder.length() - 1);
-					localStringBuilder.append(");");
-					return localStringBuilder.toString();
-				}
-				String[] arrayOfString = paramArrayOfString[j];
-				localStringBuilder.append(' ');
-				localStringBuilder.append(arrayOfString[0]);
-				localStringBuilder.append(' ');
-				localStringBuilder.append(arrayOfString[1]);
-				localStringBuilder.append(',');
-			}
-		}
-
-		/**
+    private static final String DATABASE_NAME = "DownloadsDB";
+    private static final int DATABASE_VERSION = 7;
+    public static final String LOG_TAG = DownloadsDB.class.getName();
+    final SQLiteOpenHelper mHelper;
+    SQLiteStatement mGetDownloadByIndex;
+    SQLiteStatement mUpdateCurrentBytes;
+    private static DownloadsDB mDownloadsDB;
+    long mMetadataRowID = -1;
+    int mVersionCode = -1;
+    int mStatus = -1;
+    int mFlags;
+
+    static public synchronized DownloadsDB getDB(Context paramContext) {
+        if (null == mDownloadsDB) {
+            return new DownloadsDB(paramContext);
+        }
+        return mDownloadsDB;
+    }
+
+    private SQLiteStatement getDownloadByIndexStatement() {
+        if (null == mGetDownloadByIndex) {
+            mGetDownloadByIndex = mHelper.getReadableDatabase().compileStatement(
+                    "SELECT " + BaseColumns._ID + " FROM "
+                            + DownloadColumns.TABLE_NAME + " WHERE "
+                            + DownloadColumns.INDEX + " = ?");
+        }
+        return mGetDownloadByIndex;
+    }
+
+    private SQLiteStatement getUpdateCurrentBytesStatement() {
+        if (null == mUpdateCurrentBytes) {
+            mUpdateCurrentBytes = mHelper.getReadableDatabase().compileStatement(
+                    "UPDATE " + DownloadColumns.TABLE_NAME + " SET " + DownloadColumns.CURRENTBYTES
+                            + " = ?" +
+                            " WHERE " + DownloadColumns.INDEX + " = ?");
+        }
+        return mUpdateCurrentBytes;
+    }
+
+    private DownloadsDB(Context paramContext) {
+        this.mHelper = new DownloadsContentDBHelper(paramContext);
+        final SQLiteDatabase sqldb = mHelper.getReadableDatabase();
+        // Query for the version code, the row ID of the metadata (for future
+        // updating) the status and the flags
+        Cursor cur = sqldb.rawQuery("SELECT " +
+                MetadataColumns.APKVERSION + "," +
+                BaseColumns._ID + "," +
+                MetadataColumns.DOWNLOAD_STATUS + "," +
+                MetadataColumns.FLAGS +
+                " FROM "
+                + MetadataColumns.TABLE_NAME + " LIMIT 1", null);
+        if (null != cur && cur.moveToFirst()) {
+            mVersionCode = cur.getInt(0);
+            mMetadataRowID = cur.getLong(1);
+            mStatus = cur.getInt(2);
+            mFlags = cur.getInt(3);
+            cur.close();
+        }
+        mDownloadsDB = this;
+    }
+
+    protected DownloadInfo getDownloadInfoByFileName(String fileName) {
+        final SQLiteDatabase sqldb = mHelper.getReadableDatabase();
+        Cursor itemcur = null;
+        try {
+            itemcur = sqldb.query(DownloadColumns.TABLE_NAME, DC_PROJECTION,
+                    DownloadColumns.FILENAME + " = ?",
+                    new String[] {
+                        fileName
+                    }, null, null, null);
+            if (null != itemcur && itemcur.moveToFirst()) {
+                return getDownloadInfoFromCursor(itemcur);
+            }
+        } finally {
+            if (null != itemcur)
+                itemcur.close();
+        }
+        return null;
+    }
+
+    public long getIDForDownloadInfo(final DownloadInfo di) {
+        return getIDByIndex(di.mIndex);
+    }
+
+    public long getIDByIndex(int index) {
+        SQLiteStatement downloadByIndex = getDownloadByIndexStatement();
+        downloadByIndex.clearBindings();
+        downloadByIndex.bindLong(1, index);
+        try {
+            return downloadByIndex.simpleQueryForLong();
+        } catch (SQLiteDoneException e) {
+            return -1;
+        }
+    }
+
+    public void updateDownloadCurrentBytes(final DownloadInfo di) {
+        SQLiteStatement downloadCurrentBytes = getUpdateCurrentBytesStatement();
+        downloadCurrentBytes.clearBindings();
+        downloadCurrentBytes.bindLong(1, di.mCurrentBytes);
+        downloadCurrentBytes.bindLong(2, di.mIndex);
+        downloadCurrentBytes.execute();
+    }
+
+    public void close() {
+        this.mHelper.close();
+    }
+
+    protected static class DownloadsContentDBHelper extends SQLiteOpenHelper {
+        DownloadsContentDBHelper(Context paramContext) {
+            super(paramContext, DATABASE_NAME, null, DATABASE_VERSION);
+        }
+
+        private String createTableQueryFromArray(String paramString,
+                String[][] paramArrayOfString) {
+            StringBuilder localStringBuilder = new StringBuilder();
+            localStringBuilder.append("CREATE TABLE ");
+            localStringBuilder.append(paramString);
+            localStringBuilder.append(" (");
+            int i = paramArrayOfString.length;
+            for (int j = 0;; j++) {
+                if (j >= i) {
+                    localStringBuilder
+                            .setLength(localStringBuilder.length() - 1);
+                    localStringBuilder.append(");");
+                    return localStringBuilder.toString();
+                }
+                String[] arrayOfString = paramArrayOfString[j];
+                localStringBuilder.append(' ');
+                localStringBuilder.append(arrayOfString[0]);
+                localStringBuilder.append(' ');
+                localStringBuilder.append(arrayOfString[1]);
+                localStringBuilder.append(',');
+            }
+        }
+
+        /**
          * These two arrays must match and have the same order. For every Schema
          * there must be a corresponding table name.
          */
-		static final private String[][][] sSchemas = {
-			DownloadColumns.SCHEMA, MetadataColumns.SCHEMA
-		};
+        static final private String[][][] sSchemas = {
+                DownloadColumns.SCHEMA, MetadataColumns.SCHEMA
+        };
 
-		static final private String[] sTables = {
-			DownloadColumns.TABLE_NAME, MetadataColumns.TABLE_NAME
-		};
+        static final private String[] sTables = {
+                DownloadColumns.TABLE_NAME, MetadataColumns.TABLE_NAME
+        };
 
-		/**
+        /**
          * Goes through all of the tables in sTables and drops each table if it
          * exists. Altered to no longer make use of reflection.
          */
-		private void dropTables(SQLiteDatabase paramSQLiteDatabase) {
-			for (String table : sTables) {
-				try {
-					paramSQLiteDatabase.execSQL("DROP TABLE IF EXISTS " + table);
-				} catch (Exception localException) {
-					localException.printStackTrace();
-				}
-			}
-		}
-
-		/**
+        private void dropTables(SQLiteDatabase paramSQLiteDatabase) {
+            for (String table : sTables) {
+                try {
+                    paramSQLiteDatabase.execSQL("DROP TABLE IF EXISTS " + table);
+                } catch (Exception localException) {
+                    localException.printStackTrace();
+                }
+            }
+        }
+
+        /**
          * Goes through all of the tables in sTables and creates a database with
          * the corresponding schema described in sSchemas. Altered to no longer
          * make use of reflection.
          */
-		public void onCreate(SQLiteDatabase paramSQLiteDatabase) {
-			int numSchemas = sSchemas.length;
-			for (int i = 0; i < numSchemas; i++) {
-				try {
-					String[][] schema = (String[][])sSchemas[i];
-					paramSQLiteDatabase.execSQL(createTableQueryFromArray(
-							sTables[i], schema));
-				} catch (Exception localException) {
-					while (true)
-						localException.printStackTrace();
-				}
-			}
-		}
-
-		public void onUpgrade(SQLiteDatabase paramSQLiteDatabase,
-				int paramInt1, int paramInt2) {
-			Log.w(DownloadsContentDBHelper.class.getName(),
-					"Upgrading database from version " + paramInt1 + " to " + paramInt2 + ", which will destroy all old data");
-			dropTables(paramSQLiteDatabase);
-			onCreate(paramSQLiteDatabase);
-		}
-	}
-
-	public static class MetadataColumns implements BaseColumns {
-		public static final String APKVERSION = "APKVERSION";
-		public static final String DOWNLOAD_STATUS = "DOWNLOADSTATUS";
-		public static final String FLAGS = "DOWNLOADFLAGS";
-
-		public static final String[][] SCHEMA = {
-			{ BaseColumns._ID, "INTEGER PRIMARY KEY" },
-			{ APKVERSION, "INTEGER" }, { DOWNLOAD_STATUS, "INTEGER" },
-			{ FLAGS, "INTEGER" }
-		};
-		public static final String TABLE_NAME = "MetadataColumns";
-		public static final String _ID = "MetadataColumns._id";
-	}
-
-	public static class DownloadColumns implements BaseColumns {
-		public static final String INDEX = "FILEIDX";
-		public static final String URI = "URI";
-		public static final String FILENAME = "FN";
-		public static final String ETAG = "ETAG";
-
-		public static final String TOTALBYTES = "TOTALBYTES";
-		public static final String CURRENTBYTES = "CURRENTBYTES";
-		public static final String LASTMOD = "LASTMOD";
-
-		public static final String STATUS = "STATUS";
-		public static final String CONTROL = "CONTROL";
-		public static final String NUM_FAILED = "FAILCOUNT";
-		public static final String RETRY_AFTER = "RETRYAFTER";
-		public static final String REDIRECT_COUNT = "REDIRECTCOUNT";
-
-		public static final String[][] SCHEMA = {
-			{ BaseColumns._ID, "INTEGER PRIMARY KEY" },
-			{ INDEX, "INTEGER UNIQUE" }, { URI, "TEXT" },
-			{ FILENAME, "TEXT UNIQUE" }, { ETAG, "TEXT" },
-			{ TOTALBYTES, "INTEGER" }, { CURRENTBYTES, "INTEGER" },
-			{ LASTMOD, "INTEGER" }, { STATUS, "INTEGER" },
-			{ CONTROL, "INTEGER" }, { NUM_FAILED, "INTEGER" },
-			{ RETRY_AFTER, "INTEGER" }, { REDIRECT_COUNT, "INTEGER" }
-		};
-		public static final String TABLE_NAME = "DownloadColumns";
-		public static final String _ID = "DownloadColumns._id";
-	}
-
-	private static final String[] DC_PROJECTION = {
-		DownloadColumns.FILENAME,
-		DownloadColumns.URI, DownloadColumns.ETAG,
-		DownloadColumns.TOTALBYTES, DownloadColumns.CURRENTBYTES,
-		DownloadColumns.LASTMOD, DownloadColumns.STATUS,
-		DownloadColumns.CONTROL, DownloadColumns.NUM_FAILED,
-		DownloadColumns.RETRY_AFTER, DownloadColumns.REDIRECT_COUNT,
-		DownloadColumns.INDEX
-	};
-
-	private static final int FILENAME_IDX = 0;
-	private static final int URI_IDX = 1;
-	private static final int ETAG_IDX = 2;
-	private static final int TOTALBYTES_IDX = 3;
-	private static final int CURRENTBYTES_IDX = 4;
-	private static final int LASTMOD_IDX = 5;
-	private static final int STATUS_IDX = 6;
-	private static final int CONTROL_IDX = 7;
-	private static final int NUM_FAILED_IDX = 8;
-	private static final int RETRY_AFTER_IDX = 9;
-	private static final int REDIRECT_COUNT_IDX = 10;
-	private static final int INDEX_IDX = 11;
-
-	/**
+        public void onCreate(SQLiteDatabase paramSQLiteDatabase) {
+            int numSchemas = sSchemas.length;
+            for (int i = 0; i < numSchemas; i++) {
+                try {
+                    String[][] schema = (String[][]) sSchemas[i];
+                    paramSQLiteDatabase.execSQL(createTableQueryFromArray(
+                            sTables[i], schema));
+                } catch (Exception localException) {
+                    while (true)
+                        localException.printStackTrace();
+                }
+            }
+        }
+
+        public void onUpgrade(SQLiteDatabase paramSQLiteDatabase,
+                int paramInt1, int paramInt2) {
+            Log.w(DownloadsContentDBHelper.class.getName(),
+                    "Upgrading database from version " + paramInt1 + " to "
+                            + paramInt2 + ", which will destroy all old data");
+            dropTables(paramSQLiteDatabase);
+            onCreate(paramSQLiteDatabase);
+        }
+    }
+
+    public static class MetadataColumns implements BaseColumns {
+        public static final String APKVERSION = "APKVERSION";
+        public static final String DOWNLOAD_STATUS = "DOWNLOADSTATUS";
+        public static final String FLAGS = "DOWNLOADFLAGS";
+
+        public static final String[][] SCHEMA = {
+                {
+                        BaseColumns._ID, "INTEGER PRIMARY KEY"
+                },
+                {
+                        APKVERSION, "INTEGER"
+                }, {
+                        DOWNLOAD_STATUS, "INTEGER"
+                },
+                {
+                        FLAGS, "INTEGER"
+                }
+        };
+        public static final String TABLE_NAME = "MetadataColumns";
+        public static final String _ID = "MetadataColumns._id";
+    }
+
+    public static class DownloadColumns implements BaseColumns {
+        public static final String INDEX = "FILEIDX";
+        public static final String URI = "URI";
+        public static final String FILENAME = "FN";
+        public static final String ETAG = "ETAG";
+
+        public static final String TOTALBYTES = "TOTALBYTES";
+        public static final String CURRENTBYTES = "CURRENTBYTES";
+        public static final String LASTMOD = "LASTMOD";
+
+        public static final String STATUS = "STATUS";
+        public static final String CONTROL = "CONTROL";
+        public static final String NUM_FAILED = "FAILCOUNT";
+        public static final String RETRY_AFTER = "RETRYAFTER";
+        public static final String REDIRECT_COUNT = "REDIRECTCOUNT";
+
+        public static final String[][] SCHEMA = {
+                {
+                        BaseColumns._ID, "INTEGER PRIMARY KEY"
+                },
+                {
+                        INDEX, "INTEGER UNIQUE"
+                }, {
+                        URI, "TEXT"
+                },
+                {
+                        FILENAME, "TEXT UNIQUE"
+                }, {
+                        ETAG, "TEXT"
+                },
+                {
+                        TOTALBYTES, "INTEGER"
+                }, {
+                        CURRENTBYTES, "INTEGER"
+                },
+                {
+                        LASTMOD, "INTEGER"
+                }, {
+                        STATUS, "INTEGER"
+                },
+                {
+                        CONTROL, "INTEGER"
+                }, {
+                        NUM_FAILED, "INTEGER"
+                },
+                {
+                        RETRY_AFTER, "INTEGER"
+                }, {
+                        REDIRECT_COUNT, "INTEGER"
+                }
+        };
+        public static final String TABLE_NAME = "DownloadColumns";
+        public static final String _ID = "DownloadColumns._id";
+    }
+
+    private static final String[] DC_PROJECTION = {
+            DownloadColumns.FILENAME,
+            DownloadColumns.URI, DownloadColumns.ETAG,
+            DownloadColumns.TOTALBYTES, DownloadColumns.CURRENTBYTES,
+            DownloadColumns.LASTMOD, DownloadColumns.STATUS,
+            DownloadColumns.CONTROL, DownloadColumns.NUM_FAILED,
+            DownloadColumns.RETRY_AFTER, DownloadColumns.REDIRECT_COUNT,
+            DownloadColumns.INDEX
+    };
+
+    private static final int FILENAME_IDX = 0;
+    private static final int URI_IDX = 1;
+    private static final int ETAG_IDX = 2;
+    private static final int TOTALBYTES_IDX = 3;
+    private static final int CURRENTBYTES_IDX = 4;
+    private static final int LASTMOD_IDX = 5;
+    private static final int STATUS_IDX = 6;
+    private static final int CONTROL_IDX = 7;
+    private static final int NUM_FAILED_IDX = 8;
+    private static final int RETRY_AFTER_IDX = 9;
+    private static final int REDIRECT_COUNT_IDX = 10;
+    private static final int INDEX_IDX = 11;
+
+    /**
      * This function will add a new file to the database if it does not exist.
      *
      * @param di DownloadInfo that we wish to store
      * @return the row id of the record to be updated/inserted, or -1
      */
-	public boolean updateDownload(DownloadInfo di) {
-		ContentValues cv = new ContentValues();
-		cv.put(DownloadColumns.INDEX, di.mIndex);
-		cv.put(DownloadColumns.FILENAME, di.mFileName);
-		cv.put(DownloadColumns.URI, di.mUri);
-		cv.put(DownloadColumns.ETAG, di.mETag);
-		cv.put(DownloadColumns.TOTALBYTES, di.mTotalBytes);
-		cv.put(DownloadColumns.CURRENTBYTES, di.mCurrentBytes);
-		cv.put(DownloadColumns.LASTMOD, di.mLastMod);
-		cv.put(DownloadColumns.STATUS, di.mStatus);
-		cv.put(DownloadColumns.CONTROL, di.mControl);
-		cv.put(DownloadColumns.NUM_FAILED, di.mNumFailed);
-		cv.put(DownloadColumns.RETRY_AFTER, di.mRetryAfter);
-		cv.put(DownloadColumns.REDIRECT_COUNT, di.mRedirectCount);
-		return updateDownload(di, cv);
-	}
-
-	public boolean updateDownload(DownloadInfo di, ContentValues cv) {
-		long id = di == null ? -1 : getIDForDownloadInfo(di);
-		try {
-			final SQLiteDatabase sqldb = mHelper.getWritableDatabase();
-			if (id != -1) {
-				if (1 != sqldb.update(DownloadColumns.TABLE_NAME,
-								 cv, DownloadColumns._ID + " = " + id, null)) {
-					return false;
-				}
-			} else {
-				return -1 != sqldb.insert(DownloadColumns.TABLE_NAME,
-									 DownloadColumns.URI, cv);
-			}
-		} catch (android.database.sqlite.SQLiteException ex) {
-			ex.printStackTrace();
-		}
-		return false;
-	}
-
-	public int getLastCheckedVersionCode() {
-		return mVersionCode;
-	}
-
-	public boolean isDownloadRequired() {
-		final SQLiteDatabase sqldb = mHelper.getReadableDatabase();
-		Cursor cur = sqldb.rawQuery("SELECT Count(*) FROM " + DownloadColumns.TABLE_NAME + " WHERE " + DownloadColumns.STATUS + " <> 0", null);
-		try {
-			if (null != cur && cur.moveToFirst()) {
-				return 0 == cur.getInt(0);
-			}
-		} finally {
-			if (null != cur)
-				cur.close();
-		}
-		return true;
-	}
-
-	public int getFlags() {
-		return mFlags;
-	}
-
-	public boolean updateFlags(int flags) {
-		if (mFlags != flags) {
-			ContentValues cv = new ContentValues();
-			cv.put(MetadataColumns.FLAGS, flags);
-			if (updateMetadata(cv)) {
-				mFlags = flags;
-				return true;
-			} else {
-				return false;
-			}
-		} else {
-			return true;
-		}
-	};
-
-	public boolean updateStatus(int status) {
-		if (mStatus != status) {
-			ContentValues cv = new ContentValues();
-			cv.put(MetadataColumns.DOWNLOAD_STATUS, status);
-			if (updateMetadata(cv)) {
-				mStatus = status;
-				return true;
-			} else {
-				return false;
-			}
-		} else {
-			return true;
-		}
-	};
-
-	public boolean updateMetadata(ContentValues cv) {
-		final SQLiteDatabase sqldb = mHelper.getWritableDatabase();
-		if (-1 == this.mMetadataRowID) {
-			long newID = sqldb.insert(MetadataColumns.TABLE_NAME,
-					MetadataColumns.APKVERSION, cv);
-			if (-1 == newID)
-				return false;
-			mMetadataRowID = newID;
-		} else {
-			if (0 == sqldb.update(MetadataColumns.TABLE_NAME, cv,
-							 BaseColumns._ID + " = " + mMetadataRowID, null))
-				return false;
-		}
-		return true;
-	}
-
-	public boolean updateMetadata(int apkVersion, int downloadStatus) {
-		ContentValues cv = new ContentValues();
-		cv.put(MetadataColumns.APKVERSION, apkVersion);
-		cv.put(MetadataColumns.DOWNLOAD_STATUS, downloadStatus);
-		if (updateMetadata(cv)) {
-			mVersionCode = apkVersion;
-			mStatus = downloadStatus;
-			return true;
-		} else {
-			return false;
-		}
-	};
-
-	public boolean updateFromDb(DownloadInfo di) {
-		final SQLiteDatabase sqldb = mHelper.getReadableDatabase();
-		Cursor cur = null;
-		try {
-			cur = sqldb.query(DownloadColumns.TABLE_NAME, DC_PROJECTION,
-					DownloadColumns.FILENAME + "= ?",
-					new String[] {
-							di.mFileName },
-					null, null, null);
-			if (null != cur && cur.moveToFirst()) {
-				setDownloadInfoFromCursor(di, cur);
-				return true;
-			}
-			return false;
-		} finally {
-			if (null != cur) {
-				cur.close();
-			}
-		}
-	}
-
-	public void setDownloadInfoFromCursor(DownloadInfo di, Cursor cur) {
-		di.mUri = cur.getString(URI_IDX);
-		di.mETag = cur.getString(ETAG_IDX);
-		di.mTotalBytes = cur.getLong(TOTALBYTES_IDX);
-		di.mCurrentBytes = cur.getLong(CURRENTBYTES_IDX);
-		di.mLastMod = cur.getLong(LASTMOD_IDX);
-		di.mStatus = cur.getInt(STATUS_IDX);
-		di.mControl = cur.getInt(CONTROL_IDX);
-		di.mNumFailed = cur.getInt(NUM_FAILED_IDX);
-		di.mRetryAfter = cur.getInt(RETRY_AFTER_IDX);
-		di.mRedirectCount = cur.getInt(REDIRECT_COUNT_IDX);
-	}
-
-	public DownloadInfo getDownloadInfoFromCursor(Cursor cur) {
-		DownloadInfo di = new DownloadInfo(cur.getInt(INDEX_IDX),
-				cur.getString(FILENAME_IDX), this.getClass().getPackage().getName());
-		setDownloadInfoFromCursor(di, cur);
-		return di;
-	}
-
-	public DownloadInfo[] getDownloads() {
-		final SQLiteDatabase sqldb = mHelper.getReadableDatabase();
-		Cursor cur = null;
-		try {
-			cur = sqldb.query(DownloadColumns.TABLE_NAME, DC_PROJECTION, null,
-					null, null, null, null);
-			if (null != cur && cur.moveToFirst()) {
-				DownloadInfo[] retInfos = new DownloadInfo[cur.getCount()];
-				int idx = 0;
-				do {
-					DownloadInfo di = getDownloadInfoFromCursor(cur);
-					retInfos[idx++] = di;
-				} while (cur.moveToNext());
-				return retInfos;
-			}
-			return null;
-		} finally {
-			if (null != cur) {
-				cur.close();
-			}
-		}
-	}
+    public boolean updateDownload(DownloadInfo di) {
+        ContentValues cv = new ContentValues();
+        cv.put(DownloadColumns.INDEX, di.mIndex);
+        cv.put(DownloadColumns.FILENAME, di.mFileName);
+        cv.put(DownloadColumns.URI, di.mUri);
+        cv.put(DownloadColumns.ETAG, di.mETag);
+        cv.put(DownloadColumns.TOTALBYTES, di.mTotalBytes);
+        cv.put(DownloadColumns.CURRENTBYTES, di.mCurrentBytes);
+        cv.put(DownloadColumns.LASTMOD, di.mLastMod);
+        cv.put(DownloadColumns.STATUS, di.mStatus);
+        cv.put(DownloadColumns.CONTROL, di.mControl);
+        cv.put(DownloadColumns.NUM_FAILED, di.mNumFailed);
+        cv.put(DownloadColumns.RETRY_AFTER, di.mRetryAfter);
+        cv.put(DownloadColumns.REDIRECT_COUNT, di.mRedirectCount);
+        return updateDownload(di, cv);
+    }
+
+    public boolean updateDownload(DownloadInfo di, ContentValues cv) {
+        long id = di == null ? -1 : getIDForDownloadInfo(di);
+        try {
+            final SQLiteDatabase sqldb = mHelper.getWritableDatabase();
+            if (id != -1) {
+                if (1 != sqldb.update(DownloadColumns.TABLE_NAME,
+                        cv, DownloadColumns._ID + " = " + id, null)) {
+                    return false;
+                }
+            } else {
+                return -1 != sqldb.insert(DownloadColumns.TABLE_NAME,
+                        DownloadColumns.URI, cv);
+            }
+        } catch (android.database.sqlite.SQLiteException ex) {
+            ex.printStackTrace();
+        }
+        return false;
+    }
+
+    public int getLastCheckedVersionCode() {
+        return mVersionCode;
+    }
+
+    public boolean isDownloadRequired() {
+        final SQLiteDatabase sqldb = mHelper.getReadableDatabase();
+        Cursor cur = sqldb.rawQuery("SELECT Count(*) FROM "
+                + DownloadColumns.TABLE_NAME + " WHERE "
+                + DownloadColumns.STATUS + " <> 0", null);
+        try {
+            if (null != cur && cur.moveToFirst()) {
+                return 0 == cur.getInt(0);
+            }
+        } finally {
+            if (null != cur)
+                cur.close();
+        }
+        return true;
+    }
+
+    public int getFlags() {
+        return mFlags;
+    }
+
+    public boolean updateFlags(int flags) {
+        if (mFlags != flags) {
+            ContentValues cv = new ContentValues();
+            cv.put(MetadataColumns.FLAGS, flags);
+            if (updateMetadata(cv)) {
+                mFlags = flags;
+                return true;
+            } else {
+                return false;
+            }
+        } else {
+            return true;
+        }
+    };
+
+    public boolean updateStatus(int status) {
+        if (mStatus != status) {
+            ContentValues cv = new ContentValues();
+            cv.put(MetadataColumns.DOWNLOAD_STATUS, status);
+            if (updateMetadata(cv)) {
+                mStatus = status;
+                return true;
+            } else {
+                return false;
+            }
+        } else {
+            return true;
+        }
+    };
+
+    public boolean updateMetadata(ContentValues cv) {
+        final SQLiteDatabase sqldb = mHelper.getWritableDatabase();
+        if (-1 == this.mMetadataRowID) {
+            long newID = sqldb.insert(MetadataColumns.TABLE_NAME,
+                    MetadataColumns.APKVERSION, cv);
+            if (-1 == newID)
+                return false;
+            mMetadataRowID = newID;
+        } else {
+            if (0 == sqldb.update(MetadataColumns.TABLE_NAME, cv,
+                    BaseColumns._ID + " = " + mMetadataRowID, null))
+                return false;
+        }
+        return true;
+    }
+
+    public boolean updateMetadata(int apkVersion, int downloadStatus) {
+        ContentValues cv = new ContentValues();
+        cv.put(MetadataColumns.APKVERSION, apkVersion);
+        cv.put(MetadataColumns.DOWNLOAD_STATUS, downloadStatus);
+        if (updateMetadata(cv)) {
+            mVersionCode = apkVersion;
+            mStatus = downloadStatus;
+            return true;
+        } else {
+            return false;
+        }
+    };
+
+    public boolean updateFromDb(DownloadInfo di) {
+        final SQLiteDatabase sqldb = mHelper.getReadableDatabase();
+        Cursor cur = null;
+        try {
+            cur = sqldb.query(DownloadColumns.TABLE_NAME, DC_PROJECTION,
+                    DownloadColumns.FILENAME + "= ?",
+                    new String[] {
+                        di.mFileName
+                    }, null, null, null);
+            if (null != cur && cur.moveToFirst()) {
+                setDownloadInfoFromCursor(di, cur);
+                return true;
+            }
+            return false;
+        } finally {
+            if (null != cur) {
+                cur.close();
+            }
+        }
+    }
+
+    public void setDownloadInfoFromCursor(DownloadInfo di, Cursor cur) {
+        di.mUri = cur.getString(URI_IDX);
+        di.mETag = cur.getString(ETAG_IDX);
+        di.mTotalBytes = cur.getLong(TOTALBYTES_IDX);
+        di.mCurrentBytes = cur.getLong(CURRENTBYTES_IDX);
+        di.mLastMod = cur.getLong(LASTMOD_IDX);
+        di.mStatus = cur.getInt(STATUS_IDX);
+        di.mControl = cur.getInt(CONTROL_IDX);
+        di.mNumFailed = cur.getInt(NUM_FAILED_IDX);
+        di.mRetryAfter = cur.getInt(RETRY_AFTER_IDX);
+        di.mRedirectCount = cur.getInt(REDIRECT_COUNT_IDX);
+    }
+
+    public DownloadInfo getDownloadInfoFromCursor(Cursor cur) {
+        DownloadInfo di = new DownloadInfo(cur.getInt(INDEX_IDX),
+                cur.getString(FILENAME_IDX), this.getClass().getPackage()
+                        .getName());
+        setDownloadInfoFromCursor(di, cur);
+        return di;
+    }
+
+    public DownloadInfo[] getDownloads() {
+        final SQLiteDatabase sqldb = mHelper.getReadableDatabase();
+        Cursor cur = null;
+        try {
+            cur = sqldb.query(DownloadColumns.TABLE_NAME, DC_PROJECTION, null,
+                    null, null, null, null);
+            if (null != cur && cur.moveToFirst()) {
+                DownloadInfo[] retInfos = new DownloadInfo[cur.getCount()];
+                int idx = 0;
+                do {
+                    DownloadInfo di = getDownloadInfoFromCursor(cur);
+                    retInfos[idx++] = di;
+                } while (cur.moveToNext());
+                return retInfos;
+            }
+            return null;
+        } finally {
+            if (null != cur) {
+                cur.close();
+            }
+        }
+    }
+
 }

+ 152 - 143
platform/android/java/src/com/google/android/vending/expansion/downloader/impl/HttpDateTime.java

@@ -27,7 +27,7 @@ import java.util.regex.Pattern;
  */
 public final class HttpDateTime {
 
-	/*
+    /*
      * Regular expression for parsing HTTP-date. Wdy, DD Mon YYYY HH:MM:SS GMT
      * RFC 822, updated by RFC 1123 Weekday, DD-Mon-YY HH:MM:SS GMT RFC 850,
      * obsoleted by RFC 1036 Wdy Mon DD HH:MM:SS YYYY ANSI C's asctime() format
@@ -37,155 +37,164 @@ public final class HttpDateTime {
      * (SP)D HH:MM:SS YYYY Wdy Mon DD HH:MM:SS YYYY GMT HH can be H if the first
      * digit is zero. Mon can be the full name of the month.
      */
-	private static final String HTTP_DATE_RFC_REGEXP =
-			"([0-9]{1,2})[- ]([A-Za-z]{3,9})[- ]([0-9]{2,4})[ ]"
-			+ "([0-9]{1,2}:[0-9][0-9]:[0-9][0-9])";
+    private static final String HTTP_DATE_RFC_REGEXP =
+            "([0-9]{1,2})[- ]([A-Za-z]{3,9})[- ]([0-9]{2,4})[ ]"
+                    + "([0-9]{1,2}:[0-9][0-9]:[0-9][0-9])";
 
-	private static final String HTTP_DATE_ANSIC_REGEXP =
-			"[ ]([A-Za-z]{3,9})[ ]+([0-9]{1,2})[ ]"
-			+ "([0-9]{1,2}:[0-9][0-9]:[0-9][0-9])[ ]([0-9]{2,4})";
+    private static final String HTTP_DATE_ANSIC_REGEXP =
+            "[ ]([A-Za-z]{3,9})[ ]+([0-9]{1,2})[ ]"
+                    + "([0-9]{1,2}:[0-9][0-9]:[0-9][0-9])[ ]([0-9]{2,4})";
 
-	/**
+    /**
      * The compiled version of the HTTP-date regular expressions.
      */
-	private static final Pattern HTTP_DATE_RFC_PATTERN =
-			Pattern.compile(HTTP_DATE_RFC_REGEXP);
-	private static final Pattern HTTP_DATE_ANSIC_PATTERN =
-			Pattern.compile(HTTP_DATE_ANSIC_REGEXP);
-
-	private static class TimeOfDay {
-		TimeOfDay(int h, int m, int s) {
-			this.hour = h;
-			this.minute = m;
-			this.second = s;
-		}
-
-		int hour;
-		int minute;
-		int second;
-	}
-
-	public static long parse(String timeString)
-			throws IllegalArgumentException {
-
-		int date = 1;
-		int month = Calendar.JANUARY;
-		int year = 1970;
-		TimeOfDay timeOfDay;
-
-		Matcher rfcMatcher = HTTP_DATE_RFC_PATTERN.matcher(timeString);
-		if (rfcMatcher.find()) {
-			date = getDate(rfcMatcher.group(1));
-			month = getMonth(rfcMatcher.group(2));
-			year = getYear(rfcMatcher.group(3));
-			timeOfDay = getTime(rfcMatcher.group(4));
-		} else {
-			Matcher ansicMatcher = HTTP_DATE_ANSIC_PATTERN.matcher(timeString);
-			if (ansicMatcher.find()) {
-				month = getMonth(ansicMatcher.group(1));
-				date = getDate(ansicMatcher.group(2));
-				timeOfDay = getTime(ansicMatcher.group(3));
-				year = getYear(ansicMatcher.group(4));
-			} else {
-				throw new IllegalArgumentException();
-			}
-		}
-
-		// FIXME: Y2038 BUG!
-		if (year >= 2038) {
-			year = 2038;
-			month = Calendar.JANUARY;
-			date = 1;
-		}
-
-		Time time = new Time(Time.TIMEZONE_UTC);
-		time.set(timeOfDay.second, timeOfDay.minute, timeOfDay.hour, date,
-				month, year);
-		return time.toMillis(false /* use isDst */);
-	}
-
-	private static int getDate(String dateString) {
-		if (dateString.length() == 2) {
-			return (dateString.charAt(0) - '0') * 10 + (dateString.charAt(1) - '0');
-		} else {
-			return (dateString.charAt(0) - '0');
-		}
-	}
-
-	/*
+    private static final Pattern HTTP_DATE_RFC_PATTERN =
+            Pattern.compile(HTTP_DATE_RFC_REGEXP);
+    private static final Pattern HTTP_DATE_ANSIC_PATTERN =
+            Pattern.compile(HTTP_DATE_ANSIC_REGEXP);
+
+    private static class TimeOfDay {
+        TimeOfDay(int h, int m, int s) {
+            this.hour = h;
+            this.minute = m;
+            this.second = s;
+        }
+
+        int hour;
+        int minute;
+        int second;
+    }
+
+    public static long parse(String timeString)
+            throws IllegalArgumentException {
+
+        int date = 1;
+        int month = Calendar.JANUARY;
+        int year = 1970;
+        TimeOfDay timeOfDay;
+
+        Matcher rfcMatcher = HTTP_DATE_RFC_PATTERN.matcher(timeString);
+        if (rfcMatcher.find()) {
+            date = getDate(rfcMatcher.group(1));
+            month = getMonth(rfcMatcher.group(2));
+            year = getYear(rfcMatcher.group(3));
+            timeOfDay = getTime(rfcMatcher.group(4));
+        } else {
+            Matcher ansicMatcher = HTTP_DATE_ANSIC_PATTERN.matcher(timeString);
+            if (ansicMatcher.find()) {
+                month = getMonth(ansicMatcher.group(1));
+                date = getDate(ansicMatcher.group(2));
+                timeOfDay = getTime(ansicMatcher.group(3));
+                year = getYear(ansicMatcher.group(4));
+            } else {
+                throw new IllegalArgumentException();
+            }
+        }
+
+        // FIXME: Y2038 BUG!
+        if (year >= 2038) {
+            year = 2038;
+            month = Calendar.JANUARY;
+            date = 1;
+        }
+
+        Time time = new Time(Time.TIMEZONE_UTC);
+        time.set(timeOfDay.second, timeOfDay.minute, timeOfDay.hour, date,
+                month, year);
+        return time.toMillis(false /* use isDst */);
+    }
+
+    private static int getDate(String dateString) {
+        if (dateString.length() == 2) {
+            return (dateString.charAt(0) - '0') * 10
+                    + (dateString.charAt(1) - '0');
+        } else {
+            return (dateString.charAt(0) - '0');
+        }
+    }
+
+    /*
      * jan = 9 + 0 + 13 = 22 feb = 5 + 4 + 1 = 10 mar = 12 + 0 + 17 = 29 apr = 0
      * + 15 + 17 = 32 may = 12 + 0 + 24 = 36 jun = 9 + 20 + 13 = 42 jul = 9 + 20
      * + 11 = 40 aug = 0 + 20 + 6 = 26 sep = 18 + 4 + 15 = 37 oct = 14 + 2 + 19
      * = 35 nov = 13 + 14 + 21 = 48 dec = 3 + 4 + 2 = 9
      */
-	private static int getMonth(String monthString) {
-		int hash = Character.toLowerCase(monthString.charAt(0)) +
-				   Character.toLowerCase(monthString.charAt(1)) +
-				   Character.toLowerCase(monthString.charAt(2)) - 3 * 'a';
-		switch (hash) {
-			case 22:
-				return Calendar.JANUARY;
-			case 10:
-				return Calendar.FEBRUARY;
-			case 29:
-				return Calendar.MARCH;
-			case 32:
-				return Calendar.APRIL;
-			case 36:
-				return Calendar.MAY;
-			case 42:
-				return Calendar.JUNE;
-			case 40:
-				return Calendar.JULY;
-			case 26:
-				return Calendar.AUGUST;
-			case 37:
-				return Calendar.SEPTEMBER;
-			case 35:
-				return Calendar.OCTOBER;
-			case 48:
-				return Calendar.NOVEMBER;
-			case 9:
-				return Calendar.DECEMBER;
-			default:
-				throw new IllegalArgumentException();
-		}
-	}
-
-	private static int getYear(String yearString) {
-		if (yearString.length() == 2) {
-			int year = (yearString.charAt(0) - '0') * 10 + (yearString.charAt(1) - '0');
-			if (year >= 70) {
-				return year + 1900;
-			} else {
-				return year + 2000;
-			}
-		} else if (yearString.length() == 3) {
-			// According to RFC 2822, three digit years should be added to 1900.
-			int year = (yearString.charAt(0) - '0') * 100 + (yearString.charAt(1) - '0') * 10 + (yearString.charAt(2) - '0');
-			return year + 1900;
-		} else if (yearString.length() == 4) {
-			return (yearString.charAt(0) - '0') * 1000 + (yearString.charAt(1) - '0') * 100 + (yearString.charAt(2) - '0') * 10 + (yearString.charAt(3) - '0');
-		} else {
-			return 1970;
-		}
-	}
-
-	private static TimeOfDay getTime(String timeString) {
-		// HH might be H
-		int i = 0;
-		int hour = timeString.charAt(i++) - '0';
-		if (timeString.charAt(i) != ':')
-			hour = hour * 10 + (timeString.charAt(i++) - '0');
-		// Skip ':'
-		i++;
-
-		int minute = (timeString.charAt(i++) - '0') * 10 + (timeString.charAt(i++) - '0');
-		// Skip ':'
-		i++;
-
-		int second = (timeString.charAt(i++) - '0') * 10 + (timeString.charAt(i++) - '0');
-
-		return new TimeOfDay(hour, minute, second);
-	}
+    private static int getMonth(String monthString) {
+        int hash = Character.toLowerCase(monthString.charAt(0)) +
+                Character.toLowerCase(monthString.charAt(1)) +
+                Character.toLowerCase(monthString.charAt(2)) - 3 * 'a';
+        switch (hash) {
+            case 22:
+                return Calendar.JANUARY;
+            case 10:
+                return Calendar.FEBRUARY;
+            case 29:
+                return Calendar.MARCH;
+            case 32:
+                return Calendar.APRIL;
+            case 36:
+                return Calendar.MAY;
+            case 42:
+                return Calendar.JUNE;
+            case 40:
+                return Calendar.JULY;
+            case 26:
+                return Calendar.AUGUST;
+            case 37:
+                return Calendar.SEPTEMBER;
+            case 35:
+                return Calendar.OCTOBER;
+            case 48:
+                return Calendar.NOVEMBER;
+            case 9:
+                return Calendar.DECEMBER;
+            default:
+                throw new IllegalArgumentException();
+        }
+    }
+
+    private static int getYear(String yearString) {
+        if (yearString.length() == 2) {
+            int year = (yearString.charAt(0) - '0') * 10
+                    + (yearString.charAt(1) - '0');
+            if (year >= 70) {
+                return year + 1900;
+            } else {
+                return year + 2000;
+            }
+        } else if (yearString.length() == 3) {
+            // According to RFC 2822, three digit years should be added to 1900.
+            int year = (yearString.charAt(0) - '0') * 100
+                    + (yearString.charAt(1) - '0') * 10
+                    + (yearString.charAt(2) - '0');
+            return year + 1900;
+        } else if (yearString.length() == 4) {
+            return (yearString.charAt(0) - '0') * 1000
+                    + (yearString.charAt(1) - '0') * 100
+                    + (yearString.charAt(2) - '0') * 10
+                    + (yearString.charAt(3) - '0');
+        } else {
+            return 1970;
+        }
+    }
+
+    private static TimeOfDay getTime(String timeString) {
+        // HH might be H
+        int i = 0;
+        int hour = timeString.charAt(i++) - '0';
+        if (timeString.charAt(i) != ':')
+            hour = hour * 10 + (timeString.charAt(i++) - '0');
+        // Skip ':'
+        i++;
+
+        int minute = (timeString.charAt(i++) - '0') * 10
+                + (timeString.charAt(i++) - '0');
+        // Skip ':'
+        i++;
+
+        int second = (timeString.charAt(i++) - '0') * 10
+                + (timeString.charAt(i++) - '0');
+
+        return new TimeOfDay(hour, minute, second);
+    }
 }

+ 62 - 62
platform/android/java/src/com/google/android/vending/licensing/AESObfuscator.java

@@ -36,75 +36,75 @@ import javax.crypto.spec.SecretKeySpec;
  * An Obfuscator that uses AES to encrypt data.
  */
 public class AESObfuscator implements Obfuscator {
-	private static final String UTF8 = "UTF-8";
-	private static final String KEYGEN_ALGORITHM = "PBEWITHSHAAND256BITAES-CBC-BC";
-	private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding";
-	private static final byte[] IV = { 16, 74, 71, -80, 32, 101, -47, 72, 117, -14, 0, -29, 70, 65, -12, 74 };
-	private static final String header = "com.google.android.vending.licensing.AESObfuscator-1|";
+    private static final String UTF8 = "UTF-8";
+    private static final String KEYGEN_ALGORITHM = "PBEWITHSHAAND256BITAES-CBC-BC";
+    private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding";
+    private static final byte[] IV =
+        { 16, 74, 71, -80, 32, 101, -47, 72, 117, -14, 0, -29, 70, 65, -12, 74 };
+    private static final String header = "com.google.android.vending.licensing.AESObfuscator-1|";
 
-	private Cipher mEncryptor;
-	private Cipher mDecryptor;
+    private Cipher mEncryptor;
+    private Cipher mDecryptor;
 
-	/**
+    /**
      * @param salt an array of random bytes to use for each (un)obfuscation
      * @param applicationId application identifier, e.g. the package name
      * @param deviceId device identifier. Use as many sources as possible to
      *    create this unique identifier.
      */
-	public AESObfuscator(byte[] salt, String applicationId, String deviceId) {
-		try {
-			SecretKeyFactory factory = SecretKeyFactory.getInstance(KEYGEN_ALGORITHM);
-			KeySpec keySpec =
-					new PBEKeySpec((applicationId + deviceId).toCharArray(), salt, 1024, 256);
-			SecretKey tmp = factory.generateSecret(keySpec);
-			SecretKey secret = new SecretKeySpec(tmp.getEncoded(), "AES");
-			mEncryptor = Cipher.getInstance(CIPHER_ALGORITHM);
-			mEncryptor.init(Cipher.ENCRYPT_MODE, secret, new IvParameterSpec(IV));
-			mDecryptor = Cipher.getInstance(CIPHER_ALGORITHM);
-			mDecryptor.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec(IV));
-		} catch (GeneralSecurityException e) {
-			// This can't happen on a compatible Android device.
-			throw new RuntimeException("Invalid environment", e);
-		}
-	}
+    public AESObfuscator(byte[] salt, String applicationId, String deviceId) {
+        try {
+            SecretKeyFactory factory = SecretKeyFactory.getInstance(KEYGEN_ALGORITHM);
+            KeySpec keySpec =
+                new PBEKeySpec((applicationId + deviceId).toCharArray(), salt, 1024, 256);
+            SecretKey tmp = factory.generateSecret(keySpec);
+            SecretKey secret = new SecretKeySpec(tmp.getEncoded(), "AES");
+            mEncryptor = Cipher.getInstance(CIPHER_ALGORITHM);
+            mEncryptor.init(Cipher.ENCRYPT_MODE, secret, new IvParameterSpec(IV));
+            mDecryptor = Cipher.getInstance(CIPHER_ALGORITHM);
+            mDecryptor.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec(IV));
+        } catch (GeneralSecurityException e) {
+            // This can't happen on a compatible Android device.
+            throw new RuntimeException("Invalid environment", e);
+        }
+    }
 
-	public String obfuscate(String original, String key) {
-		if (original == null) {
-			return null;
-		}
-		try {
-			// Header is appended as an integrity check
-			return Base64.encode(mEncryptor.doFinal((header + key + original).getBytes(UTF8)));
-		} catch (UnsupportedEncodingException e) {
-			throw new RuntimeException("Invalid environment", e);
-		} catch (GeneralSecurityException e) {
-			throw new RuntimeException("Invalid environment", e);
-		}
-	}
+    public String obfuscate(String original, String key) {
+        if (original == null) {
+            return null;
+        }
+        try {
+            // Header is appended as an integrity check
+            return Base64.encode(mEncryptor.doFinal((header + key + original).getBytes(UTF8)));
+        } catch (UnsupportedEncodingException e) {
+            throw new RuntimeException("Invalid environment", e);
+        } catch (GeneralSecurityException e) {
+            throw new RuntimeException("Invalid environment", e);
+        }
+    }
 
-	public String unobfuscate(String obfuscated, String key) throws ValidationException {
-		if (obfuscated == null) {
-			return null;
-		}
-		try {
-			String result = new String(mDecryptor.doFinal(Base64.decode(obfuscated)), UTF8);
-			// Check for presence of header. This serves as a final integrity check, for cases
-			// where the block size is correct during decryption.
-			int headerIndex = result.indexOf(header + key);
-			if (headerIndex != 0) {
-				throw new ValidationException("Header not found (invalid data or key)"
-											  + ":" +
-											  obfuscated);
-			}
-			return result.substring(header.length() + key.length(), result.length());
-		} catch (Base64DecoderException e) {
-			throw new ValidationException(e.getMessage() + ":" + obfuscated);
-		} catch (IllegalBlockSizeException e) {
-			throw new ValidationException(e.getMessage() + ":" + obfuscated);
-		} catch (BadPaddingException e) {
-			throw new ValidationException(e.getMessage() + ":" + obfuscated);
-		} catch (UnsupportedEncodingException e) {
-			throw new RuntimeException("Invalid environment", e);
-		}
-	}
+    public String unobfuscate(String obfuscated, String key) throws ValidationException {
+        if (obfuscated == null) {
+            return null;
+        }
+        try {
+            String result = new String(mDecryptor.doFinal(Base64.decode(obfuscated)), UTF8);
+            // Check for presence of header. This serves as a final integrity check, for cases
+            // where the block size is correct during decryption.
+            int headerIndex = result.indexOf(header+key);
+            if (headerIndex != 0) {
+                throw new ValidationException("Header not found (invalid data or key)" + ":" +
+                        obfuscated);
+            }
+            return result.substring(header.length()+key.length(), result.length());
+        } catch (Base64DecoderException e) {
+            throw new ValidationException(e.getMessage() + ":" + obfuscated);
+        } catch (IllegalBlockSizeException e) {
+            throw new ValidationException(e.getMessage() + ":" + obfuscated);
+        } catch (BadPaddingException e) {
+            throw new ValidationException(e.getMessage() + ":" + obfuscated);
+        } catch (UnsupportedEncodingException e) {
+            throw new RuntimeException("Invalid environment", e);
+        }
+    }
 }

+ 271 - 270
platform/android/java/src/com/google/android/vending/licensing/APKExpansionPolicy.java

@@ -46,73 +46,73 @@ import java.util.Vector;
  */
 public class APKExpansionPolicy implements Policy {
 
-	private static final String TAG = "APKExpansionPolicy";
-	private static final String PREFS_FILE = "com.google.android.vending.licensing.APKExpansionPolicy";
-	private static final String PREF_LAST_RESPONSE = "lastResponse";
-	private static final String PREF_VALIDITY_TIMESTAMP = "validityTimestamp";
-	private static final String PREF_RETRY_UNTIL = "retryUntil";
-	private static final String PREF_MAX_RETRIES = "maxRetries";
-	private static final String PREF_RETRY_COUNT = "retryCount";
-	private static final String PREF_LICENSING_URL = "licensingUrl";
-	private static final String DEFAULT_VALIDITY_TIMESTAMP = "0";
-	private static final String DEFAULT_RETRY_UNTIL = "0";
-	private static final String DEFAULT_MAX_RETRIES = "0";
-	private static final String DEFAULT_RETRY_COUNT = "0";
-
-	private static final long MILLIS_PER_MINUTE = 60 * 1000;
-
-	private long mValidityTimestamp;
-	private long mRetryUntil;
-	private long mMaxRetries;
-	private long mRetryCount;
-	private long mLastResponseTime = 0;
-	private int mLastResponse;
-	private String mLicensingUrl;
-	private PreferenceObfuscator mPreferences;
-	private Vector<String> mExpansionURLs = new Vector<String>();
-	private Vector<String> mExpansionFileNames = new Vector<String>();
-	private Vector<Long> mExpansionFileSizes = new Vector<Long>();
-
-	/**
+    private static final String TAG = "APKExpansionPolicy";
+    private static final String PREFS_FILE = "com.google.android.vending.licensing.APKExpansionPolicy";
+    private static final String PREF_LAST_RESPONSE = "lastResponse";
+    private static final String PREF_VALIDITY_TIMESTAMP = "validityTimestamp";
+    private static final String PREF_RETRY_UNTIL = "retryUntil";
+    private static final String PREF_MAX_RETRIES = "maxRetries";
+    private static final String PREF_RETRY_COUNT = "retryCount";
+    private static final String PREF_LICENSING_URL = "licensingUrl";
+    private static final String DEFAULT_VALIDITY_TIMESTAMP = "0";
+    private static final String DEFAULT_RETRY_UNTIL = "0";
+    private static final String DEFAULT_MAX_RETRIES = "0";
+    private static final String DEFAULT_RETRY_COUNT = "0";
+
+    private static final long MILLIS_PER_MINUTE = 60 * 1000;
+
+    private long mValidityTimestamp;
+    private long mRetryUntil;
+    private long mMaxRetries;
+    private long mRetryCount;
+    private long mLastResponseTime = 0;
+    private int mLastResponse;
+    private String mLicensingUrl;
+    private PreferenceObfuscator mPreferences;
+    private Vector<String> mExpansionURLs = new Vector<String>();
+    private Vector<String> mExpansionFileNames = new Vector<String>();
+    private Vector<Long> mExpansionFileSizes = new Vector<Long>();
+
+    /**
      * The design of the protocol supports n files. Currently the market can
      * only deliver two files. To accommodate this, we have these two constants,
      * but the order is the only relevant thing here.
      */
-	public static final int MAIN_FILE_URL_INDEX = 0;
-	public static final int PATCH_FILE_URL_INDEX = 1;
+    public static final int MAIN_FILE_URL_INDEX = 0;
+    public static final int PATCH_FILE_URL_INDEX = 1;
 
-	/**
+    /**
      * @param context The context for the current application
      * @param obfuscator An obfuscator to be used with preferences.
      */
-	public APKExpansionPolicy(Context context, Obfuscator obfuscator) {
-		// Import old values
-		SharedPreferences sp = context.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE);
-		mPreferences = new PreferenceObfuscator(sp, obfuscator);
-		mLastResponse = Integer.parseInt(
-				mPreferences.getString(PREF_LAST_RESPONSE, Integer.toString(Policy.RETRY)));
-		mValidityTimestamp = Long.parseLong(mPreferences.getString(PREF_VALIDITY_TIMESTAMP,
-				DEFAULT_VALIDITY_TIMESTAMP));
-		mRetryUntil = Long.parseLong(mPreferences.getString(PREF_RETRY_UNTIL, DEFAULT_RETRY_UNTIL));
-		mMaxRetries = Long.parseLong(mPreferences.getString(PREF_MAX_RETRIES, DEFAULT_MAX_RETRIES));
-		mRetryCount = Long.parseLong(mPreferences.getString(PREF_RETRY_COUNT, DEFAULT_RETRY_COUNT));
-		mLicensingUrl = mPreferences.getString(PREF_LICENSING_URL, null);
-	}
-
-	/**
+    public APKExpansionPolicy(Context context, Obfuscator obfuscator) {
+        // Import old values
+        SharedPreferences sp = context.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE);
+        mPreferences = new PreferenceObfuscator(sp, obfuscator);
+        mLastResponse = Integer.parseInt(
+                mPreferences.getString(PREF_LAST_RESPONSE, Integer.toString(Policy.RETRY)));
+        mValidityTimestamp = Long.parseLong(mPreferences.getString(PREF_VALIDITY_TIMESTAMP,
+                DEFAULT_VALIDITY_TIMESTAMP));
+        mRetryUntil = Long.parseLong(mPreferences.getString(PREF_RETRY_UNTIL, DEFAULT_RETRY_UNTIL));
+        mMaxRetries = Long.parseLong(mPreferences.getString(PREF_MAX_RETRIES, DEFAULT_MAX_RETRIES));
+        mRetryCount = Long.parseLong(mPreferences.getString(PREF_RETRY_COUNT, DEFAULT_RETRY_COUNT));
+        mLicensingUrl = mPreferences.getString(PREF_LICENSING_URL, null);
+    }
+
+    /**
      * We call this to guarantee that we fetch a fresh policy from the server.
      * This is to be used if the URL is invalid.
      */
-	public void resetPolicy() {
-		mPreferences.putString(PREF_LAST_RESPONSE, Integer.toString(Policy.RETRY));
-		setRetryUntil(DEFAULT_RETRY_UNTIL);
-		setMaxRetries(DEFAULT_MAX_RETRIES);
-		setRetryCount(Long.parseLong(DEFAULT_RETRY_COUNT));
-		setValidityTimestamp(DEFAULT_VALIDITY_TIMESTAMP);
-		mPreferences.commit();
-	}
-
-	/**
+    public void resetPolicy() {
+        mPreferences.putString(PREF_LAST_RESPONSE, Integer.toString(Policy.RETRY));
+        setRetryUntil(DEFAULT_RETRY_UNTIL);
+        setMaxRetries(DEFAULT_MAX_RETRIES);
+        setRetryCount(Long.parseLong(DEFAULT_RETRY_COUNT));
+        setValidityTimestamp(DEFAULT_VALIDITY_TIMESTAMP);
+        mPreferences.commit();
+    }
+
+    /**
      * Process a new response from the license server.
      * <p>
      * This data will be used for computing future policy decisions. The
@@ -129,187 +129,187 @@ public class APKExpansionPolicy implements Policy {
      * @param response the result from validating the server response
      * @param rawData the raw server response data
      */
-	public void processServerResponse(int response,
-			com.google.android.vending.licensing.ResponseData rawData) {
-
-		// Update retry counter
-		if (response != Policy.RETRY) {
-			setRetryCount(0);
-		} else {
-			setRetryCount(mRetryCount + 1);
-		}
-
-		// Update server policy data
-		Map<String, String> extras = decodeExtras(rawData);
-		if (response == Policy.LICENSED) {
-			mLastResponse = response;
-			// Reset the licensing URL since it is only applicable for NOT_LICENSED responses.
-			setLicensingUrl(null);
-			setValidityTimestamp(Long.toString(System.currentTimeMillis() + MILLIS_PER_MINUTE));
-			Set<String> keys = extras.keySet();
-			for (String key : keys) {
-				if (key.equals("VT")) {
-					setValidityTimestamp(extras.get(key));
-				} else if (key.equals("GT")) {
-					setRetryUntil(extras.get(key));
-				} else if (key.equals("GR")) {
-					setMaxRetries(extras.get(key));
-				} else if (key.startsWith("FILE_URL")) {
-					int index = Integer.parseInt(key.substring("FILE_URL".length())) - 1;
-					setExpansionURL(index, extras.get(key));
-				} else if (key.startsWith("FILE_NAME")) {
-					int index = Integer.parseInt(key.substring("FILE_NAME".length())) - 1;
-					setExpansionFileName(index, extras.get(key));
-				} else if (key.startsWith("FILE_SIZE")) {
-					int index = Integer.parseInt(key.substring("FILE_SIZE".length())) - 1;
-					setExpansionFileSize(index, Long.parseLong(extras.get(key)));
-				}
-			}
-		} else if (response == Policy.NOT_LICENSED) {
-			// Clear out stale retry params
-			setValidityTimestamp(DEFAULT_VALIDITY_TIMESTAMP);
-			setRetryUntil(DEFAULT_RETRY_UNTIL);
-			setMaxRetries(DEFAULT_MAX_RETRIES);
-			// Update the licensing URL
-			setLicensingUrl(extras.get("LU"));
-		}
-
-		setLastResponse(response);
-		mPreferences.commit();
-	}
-
-	/**
+    public void processServerResponse(int response,
+            com.google.android.vending.licensing.ResponseData rawData) {
+
+        // Update retry counter
+        if (response != Policy.RETRY) {
+            setRetryCount(0);
+        } else {
+            setRetryCount(mRetryCount + 1);
+        }
+
+        // Update server policy data
+        Map<String, String> extras = decodeExtras(rawData);
+        if (response == Policy.LICENSED) {
+            mLastResponse = response;
+            // Reset the licensing URL since it is only applicable for NOT_LICENSED responses.
+            setLicensingUrl(null);
+            setValidityTimestamp(Long.toString(System.currentTimeMillis() + MILLIS_PER_MINUTE));
+            Set<String> keys = extras.keySet();
+            for (String key : keys) {
+                if (key.equals("VT")) {
+                    setValidityTimestamp(extras.get(key));
+                } else if (key.equals("GT")) {
+                    setRetryUntil(extras.get(key));
+                } else if (key.equals("GR")) {
+                    setMaxRetries(extras.get(key));
+                } else if (key.startsWith("FILE_URL")) {
+                    int index = Integer.parseInt(key.substring("FILE_URL".length())) - 1;
+                    setExpansionURL(index, extras.get(key));
+                } else if (key.startsWith("FILE_NAME")) {
+                    int index = Integer.parseInt(key.substring("FILE_NAME".length())) - 1;
+                    setExpansionFileName(index, extras.get(key));
+                } else if (key.startsWith("FILE_SIZE")) {
+                    int index = Integer.parseInt(key.substring("FILE_SIZE".length())) - 1;
+                    setExpansionFileSize(index, Long.parseLong(extras.get(key)));
+                }
+            }
+        } else if (response == Policy.NOT_LICENSED) {
+            // Clear out stale retry params
+            setValidityTimestamp(DEFAULT_VALIDITY_TIMESTAMP);
+            setRetryUntil(DEFAULT_RETRY_UNTIL);
+            setMaxRetries(DEFAULT_MAX_RETRIES);
+            // Update the licensing URL
+            setLicensingUrl(extras.get("LU"));
+        }
+
+        setLastResponse(response);
+        mPreferences.commit();
+    }
+
+    /**
      * Set the last license response received from the server and add to
      * preferences. You must manually call PreferenceObfuscator.commit() to
      * commit these changes to disk.
      *
      * @param l the response
      */
-	private void setLastResponse(int l) {
-		mLastResponseTime = System.currentTimeMillis();
-		mLastResponse = l;
-		mPreferences.putString(PREF_LAST_RESPONSE, Integer.toString(l));
-	}
+    private void setLastResponse(int l) {
+        mLastResponseTime = System.currentTimeMillis();
+        mLastResponse = l;
+        mPreferences.putString(PREF_LAST_RESPONSE, Integer.toString(l));
+    }
 
-	/**
+    /**
      * Set the current retry count and add to preferences. You must manually
      * call PreferenceObfuscator.commit() to commit these changes to disk.
      *
      * @param c the new retry count
      */
-	private void setRetryCount(long c) {
-		mRetryCount = c;
-		mPreferences.putString(PREF_RETRY_COUNT, Long.toString(c));
-	}
+    private void setRetryCount(long c) {
+        mRetryCount = c;
+        mPreferences.putString(PREF_RETRY_COUNT, Long.toString(c));
+    }
 
-	public long getRetryCount() {
-		return mRetryCount;
-	}
+    public long getRetryCount() {
+        return mRetryCount;
+    }
 
-	/**
+    /**
      * Set the last validity timestamp (VT) received from the server and add to
      * preferences. You must manually call PreferenceObfuscator.commit() to
      * commit these changes to disk.
      *
      * @param validityTimestamp the VT string received
      */
-	private void setValidityTimestamp(String validityTimestamp) {
-		Long lValidityTimestamp;
-		try {
-			lValidityTimestamp = Long.parseLong(validityTimestamp);
-		} catch (NumberFormatException e) {
-			// No response or not parseable, expire in one minute.
-			Log.w(TAG, "License validity timestamp (VT) missing, caching for a minute");
-			lValidityTimestamp = System.currentTimeMillis() + MILLIS_PER_MINUTE;
-			validityTimestamp = Long.toString(lValidityTimestamp);
-		}
-
-		mValidityTimestamp = lValidityTimestamp;
-		mPreferences.putString(PREF_VALIDITY_TIMESTAMP, validityTimestamp);
-	}
-
-	public long getValidityTimestamp() {
-		return mValidityTimestamp;
-	}
-
-	/**
+    private void setValidityTimestamp(String validityTimestamp) {
+        Long lValidityTimestamp;
+        try {
+            lValidityTimestamp = Long.parseLong(validityTimestamp);
+        } catch (NumberFormatException e) {
+            // No response or not parseable, expire in one minute.
+            Log.w(TAG, "License validity timestamp (VT) missing, caching for a minute");
+            lValidityTimestamp = System.currentTimeMillis() + MILLIS_PER_MINUTE;
+            validityTimestamp = Long.toString(lValidityTimestamp);
+        }
+
+        mValidityTimestamp = lValidityTimestamp;
+        mPreferences.putString(PREF_VALIDITY_TIMESTAMP, validityTimestamp);
+    }
+
+    public long getValidityTimestamp() {
+        return mValidityTimestamp;
+    }
+
+    /**
      * Set the retry until timestamp (GT) received from the server and add to
      * preferences. You must manually call PreferenceObfuscator.commit() to
      * commit these changes to disk.
      *
      * @param retryUntil the GT string received
      */
-	private void setRetryUntil(String retryUntil) {
-		Long lRetryUntil;
-		try {
-			lRetryUntil = Long.parseLong(retryUntil);
-		} catch (NumberFormatException e) {
-			// No response or not parseable, expire immediately
-			Log.w(TAG, "License retry timestamp (GT) missing, grace period disabled");
-			retryUntil = "0";
-			lRetryUntil = 0l;
-		}
-
-		mRetryUntil = lRetryUntil;
-		mPreferences.putString(PREF_RETRY_UNTIL, retryUntil);
-	}
-
-	public long getRetryUntil() {
-		return mRetryUntil;
-	}
-
-	/**
+    private void setRetryUntil(String retryUntil) {
+        Long lRetryUntil;
+        try {
+            lRetryUntil = Long.parseLong(retryUntil);
+        } catch (NumberFormatException e) {
+            // No response or not parseable, expire immediately
+            Log.w(TAG, "License retry timestamp (GT) missing, grace period disabled");
+            retryUntil = "0";
+            lRetryUntil = 0l;
+        }
+
+        mRetryUntil = lRetryUntil;
+        mPreferences.putString(PREF_RETRY_UNTIL, retryUntil);
+    }
+
+    public long getRetryUntil() {
+        return mRetryUntil;
+    }
+
+    /**
      * Set the max retries value (GR) as received from the server and add to
      * preferences. You must manually call PreferenceObfuscator.commit() to
      * commit these changes to disk.
      *
      * @param maxRetries the GR string received
      */
-	private void setMaxRetries(String maxRetries) {
-		Long lMaxRetries;
-		try {
-			lMaxRetries = Long.parseLong(maxRetries);
-		} catch (NumberFormatException e) {
-			// No response or not parseable, expire immediately
-			Log.w(TAG, "Licence retry count (GR) missing, grace period disabled");
-			maxRetries = "0";
-			lMaxRetries = 0l;
-		}
-
-		mMaxRetries = lMaxRetries;
-		mPreferences.putString(PREF_MAX_RETRIES, maxRetries);
-	}
-
-	public long getMaxRetries() {
-		return mMaxRetries;
-	}
-
-	/**
+    private void setMaxRetries(String maxRetries) {
+        Long lMaxRetries;
+        try {
+            lMaxRetries = Long.parseLong(maxRetries);
+        } catch (NumberFormatException e) {
+            // No response or not parseable, expire immediately
+            Log.w(TAG, "Licence retry count (GR) missing, grace period disabled");
+            maxRetries = "0";
+            lMaxRetries = 0l;
+        }
+
+        mMaxRetries = lMaxRetries;
+        mPreferences.putString(PREF_MAX_RETRIES, maxRetries);
+    }
+
+    public long getMaxRetries() {
+        return mMaxRetries;
+    }
+
+    /**
      * Set the licensing URL that displays a Play Store UI for the user to regain app access.
      *
      * @param url the LU string received
      */
-	private void setLicensingUrl(String url) {
-		mLicensingUrl = url;
-		mPreferences.putString(PREF_LICENSING_URL, url);
-	}
+    private void setLicensingUrl(String url) {
+        mLicensingUrl = url;
+        mPreferences.putString(PREF_LICENSING_URL, url);
+    }
 
-	public String getLicensingUrl() {
-		return mLicensingUrl;
-	}
+    public String getLicensingUrl() {
+        return mLicensingUrl;
+    }
 
-	/**
+    /**
      * Gets the count of expansion URLs. Since expansionURLs are not committed
      * to preferences, this will return zero if there has been no LVL fetch
      * in the current session.
      *
      * @return the number of expansion URLs. (0,1,2)
      */
-	public int getExpansionURLCount() {
-		return mExpansionURLs.size();
-	}
+    public int getExpansionURLCount() {
+        return mExpansionURLs.size();
+    }
 
-	/**
+    /**
      * Gets the expansion URL. Since these URLs are not committed to
      * preferences, this will always return null if there has not been an LVL
      * fetch in the current session.
@@ -317,14 +317,14 @@ public class APKExpansionPolicy implements Policy {
      * @param index the index of the URL to fetch. This value will be either
      *            MAIN_FILE_URL_INDEX or PATCH_FILE_URL_INDEX
      */
-	public String getExpansionURL(int index) {
-		if (index < mExpansionURLs.size()) {
-			return mExpansionURLs.elementAt(index);
-		}
-		return null;
-	}
-
-	/**
+    public String getExpansionURL(int index) {
+        if (index < mExpansionURLs.size()) {
+            return mExpansionURLs.elementAt(index);
+        }
+        return null;
+    }
+
+    /**
      * Sets the expansion URL. Expansion URL's are not committed to preferences,
      * but are instead intended to be stored when the license response is
      * processed by the front-end.
@@ -333,42 +333,42 @@ public class APKExpansionPolicy implements Policy {
      *            MAIN_FILE_URL_INDEX or PATCH_FILE_URL_INDEX
      * @param URL the URL to set
      */
-	public void setExpansionURL(int index, String URL) {
-		if (index >= mExpansionURLs.size()) {
-			mExpansionURLs.setSize(index + 1);
-		}
-		mExpansionURLs.set(index, URL);
-	}
-
-	public String getExpansionFileName(int index) {
-		if (index < mExpansionFileNames.size()) {
-			return mExpansionFileNames.elementAt(index);
-		}
-		return null;
-	}
-
-	public void setExpansionFileName(int index, String name) {
-		if (index >= mExpansionFileNames.size()) {
-			mExpansionFileNames.setSize(index + 1);
-		}
-		mExpansionFileNames.set(index, name);
-	}
-
-	public long getExpansionFileSize(int index) {
-		if (index < mExpansionFileSizes.size()) {
-			return mExpansionFileSizes.elementAt(index);
-		}
-		return -1;
-	}
-
-	public void setExpansionFileSize(int index, long size) {
-		if (index >= mExpansionFileSizes.size()) {
-			mExpansionFileSizes.setSize(index + 1);
-		}
-		mExpansionFileSizes.set(index, size);
-	}
-
-	/**
+    public void setExpansionURL(int index, String URL) {
+        if (index >= mExpansionURLs.size()) {
+            mExpansionURLs.setSize(index + 1);
+        }
+        mExpansionURLs.set(index, URL);
+    }
+
+    public String getExpansionFileName(int index) {
+        if (index < mExpansionFileNames.size()) {
+            return mExpansionFileNames.elementAt(index);
+        }
+        return null;
+    }
+
+    public void setExpansionFileName(int index, String name) {
+        if (index >= mExpansionFileNames.size()) {
+            mExpansionFileNames.setSize(index + 1);
+        }
+        mExpansionFileNames.set(index, name);
+    }
+
+    public long getExpansionFileSize(int index) {
+        if (index < mExpansionFileSizes.size()) {
+            return mExpansionFileSizes.elementAt(index);
+        }
+        return -1;
+    }
+
+    public void setExpansionFileSize(int index, long size) {
+        if (index >= mExpansionFileSizes.size()) {
+            mExpansionFileSizes.setSize(index + 1);
+        }
+        mExpansionFileSizes.set(index, size);
+    }
+
+    /**
      * {@inheritDoc} This implementation allows access if either:<br>
      * <ol>
      * <li>a LICENSED response was received within the validity period
@@ -376,38 +376,39 @@ public class APKExpansionPolicy implements Policy {
      * the RETRY count or in the RETRY period.
      * </ol>
      */
-	public boolean allowAccess() {
-		long ts = System.currentTimeMillis();
-		if (mLastResponse == Policy.LICENSED) {
-			// Check if the LICENSED response occurred within the validity
-			// timeout.
-			if (ts <= mValidityTimestamp) {
-				// Cached LICENSED response is still valid.
-				return true;
-			}
-		} else if (mLastResponse == Policy.RETRY &&
-				   ts < mLastResponseTime + MILLIS_PER_MINUTE) {
-			// Only allow access if we are within the retry period or we haven't
-			// used up our
-			// max retries.
-			return (ts <= mRetryUntil || mRetryCount <= mMaxRetries);
-		}
-		return false;
-	}
-
-	private Map<String, String> decodeExtras(
-			com.google.android.vending.licensing.ResponseData rawData) {
-		Map<String, String> results = new HashMap<String, String>();
-		if (rawData == null) {
-			return results;
-		}
-
-		try {
-			URI rawExtras = new URI("?" + rawData.extra);
-			URIQueryDecoder.DecodeQuery(rawExtras, results);
-		} catch (URISyntaxException e) {
-			Log.w(TAG, "Invalid syntax error while decoding extras data from server.");
-		}
-		return results;
-	}
+    public boolean allowAccess() {
+        long ts = System.currentTimeMillis();
+        if (mLastResponse == Policy.LICENSED) {
+            // Check if the LICENSED response occurred within the validity
+            // timeout.
+            if (ts <= mValidityTimestamp) {
+                // Cached LICENSED response is still valid.
+                return true;
+            }
+        } else if (mLastResponse == Policy.RETRY &&
+                ts < mLastResponseTime + MILLIS_PER_MINUTE) {
+            // Only allow access if we are within the retry period or we haven't
+            // used up our
+            // max retries.
+            return (ts <= mRetryUntil || mRetryCount <= mMaxRetries);
+        }
+        return false;
+    }
+
+    private Map<String, String> decodeExtras(
+        com.google.android.vending.licensing.ResponseData rawData) {
+        Map<String, String> results = new HashMap<String, String>();
+        if (rawData == null) {
+            return results;
+        }
+
+        try {
+            URI rawExtras = new URI("?" + rawData.extra);
+            URIQueryDecoder.DecodeQuery(rawExtras, results);
+        } catch (URISyntaxException e) {
+            Log.w(TAG, "Invalid syntax error while decoding extras data from server.");
+        }
+        return results;
+    }
+
 }

+ 2 - 2
platform/android/java/src/com/google/android/vending/licensing/DeviceLimiter.java

@@ -37,11 +37,11 @@ package com.google.android.vending.licensing;
  */
 public interface DeviceLimiter {
 
-	/**
+    /**
      * Checks if this device is allowed to use the given user's license.
      *
      * @param userId the user whose license the server responded with
      * @return LICENSED if the device is allowed, NOT_LICENSED if not, RETRY if an error occurs
      */
-	int isDeviceAllowed(String userId);
+    int isDeviceAllowed(String userId);
 }

+ 0 - 100
platform/android/java/src/com/google/android/vending/licensing/ILicenseResultListener.java

@@ -1,100 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
-*/
-
-/*
- * This file is auto-generated.  DO NOT MODIFY.
- * Original file: aidl/ILicenseResultListener.aidl
- */
-package com.google.android.vending.licensing;
-import java.lang.String;
-import android.os.RemoteException;
-import android.os.IBinder;
-import android.os.IInterface;
-import android.os.Binder;
-import android.os.Parcel;
-public interface ILicenseResultListener extends android.os.IInterface {
-	/** Local-side IPC implementation stub class. */
-	public static abstract class Stub extends android.os.Binder implements com.google.android.vending.licensing.ILicenseResultListener {
-		private static final java.lang.String DESCRIPTOR = "com.android.vending.licensing.ILicenseResultListener";
-		/** Construct the stub at attach it to the interface. */
-		public Stub() {
-			this.attachInterface(this, DESCRIPTOR);
-		}
-		/**
- * Cast an IBinder object into an ILicenseResultListener interface,
- * generating a proxy if needed.
- */
-		public static com.google.android.vending.licensing.ILicenseResultListener asInterface(android.os.IBinder obj) {
-			if ((obj == null)) {
-				return null;
-			}
-			android.os.IInterface iin = (android.os.IInterface)obj.queryLocalInterface(DESCRIPTOR);
-			if (((iin != null) && (iin instanceof com.google.android.vending.licensing.ILicenseResultListener))) {
-				return ((com.google.android.vending.licensing.ILicenseResultListener)iin);
-			}
-			return new com.google.android.vending.licensing.ILicenseResultListener.Stub.Proxy(obj);
-		}
-		public android.os.IBinder asBinder() {
-			return this;
-		}
-		public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException {
-			switch (code) {
-				case INTERFACE_TRANSACTION: {
-					reply.writeString(DESCRIPTOR);
-					return true;
-				}
-				case TRANSACTION_verifyLicense: {
-					data.enforceInterface(DESCRIPTOR);
-					int _arg0;
-					_arg0 = data.readInt();
-					java.lang.String _arg1;
-					_arg1 = data.readString();
-					java.lang.String _arg2;
-					_arg2 = data.readString();
-					this.verifyLicense(_arg0, _arg1, _arg2);
-					return true;
-				}
-			}
-			return super.onTransact(code, data, reply, flags);
-		}
-		private static class Proxy implements com.google.android.vending.licensing.ILicenseResultListener {
-			private android.os.IBinder mRemote;
-			Proxy(android.os.IBinder remote) {
-				mRemote = remote;
-			}
-			public android.os.IBinder asBinder() {
-				return mRemote;
-			}
-			public java.lang.String getInterfaceDescriptor() {
-				return DESCRIPTOR;
-			}
-			public void verifyLicense(int responseCode, java.lang.String signedData, java.lang.String signature) throws android.os.RemoteException {
-				android.os.Parcel _data = android.os.Parcel.obtain();
-				try {
-					_data.writeInterfaceToken(DESCRIPTOR);
-					_data.writeInt(responseCode);
-					_data.writeString(signedData);
-					_data.writeString(signature);
-					mRemote.transact(Stub.TRANSACTION_verifyLicense, _data, null, IBinder.FLAG_ONEWAY);
-				} finally {
-					_data.recycle();
-				}
-			}
-		}
-		static final int TRANSACTION_verifyLicense = (IBinder.FIRST_CALL_TRANSACTION + 0);
-	}
-	public void verifyLicense(int responseCode, java.lang.String signedData, java.lang.String signature) throws android.os.RemoteException;
-}

+ 0 - 100
platform/android/java/src/com/google/android/vending/licensing/ILicensingService.java

@@ -1,100 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
-*/
-
-/*
- * This file is auto-generated.  DO NOT MODIFY.
- * Original file: aidl/ILicensingService.aidl
- */
-package com.google.android.vending.licensing;
-import java.lang.String;
-import android.os.RemoteException;
-import android.os.IBinder;
-import android.os.IInterface;
-import android.os.Binder;
-import android.os.Parcel;
-public interface ILicensingService extends android.os.IInterface {
-	/** Local-side IPC implementation stub class. */
-	public static abstract class Stub extends android.os.Binder implements com.google.android.vending.licensing.ILicensingService {
-		private static final java.lang.String DESCRIPTOR = "com.android.vending.licensing.ILicensingService";
-		/** Construct the stub at attach it to the interface. */
-		public Stub() {
-			this.attachInterface(this, DESCRIPTOR);
-		}
-		/**
- * Cast an IBinder object into an ILicensingService interface,
- * generating a proxy if needed.
- */
-		public static com.google.android.vending.licensing.ILicensingService asInterface(android.os.IBinder obj) {
-			if ((obj == null)) {
-				return null;
-			}
-			android.os.IInterface iin = (android.os.IInterface)obj.queryLocalInterface(DESCRIPTOR);
-			if (((iin != null) && (iin instanceof com.google.android.vending.licensing.ILicensingService))) {
-				return ((com.google.android.vending.licensing.ILicensingService)iin);
-			}
-			return new com.google.android.vending.licensing.ILicensingService.Stub.Proxy(obj);
-		}
-		public android.os.IBinder asBinder() {
-			return this;
-		}
-		public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException {
-			switch (code) {
-				case INTERFACE_TRANSACTION: {
-					reply.writeString(DESCRIPTOR);
-					return true;
-				}
-				case TRANSACTION_checkLicense: {
-					data.enforceInterface(DESCRIPTOR);
-					long _arg0;
-					_arg0 = data.readLong();
-					java.lang.String _arg1;
-					_arg1 = data.readString();
-					com.google.android.vending.licensing.ILicenseResultListener _arg2;
-					_arg2 = com.google.android.vending.licensing.ILicenseResultListener.Stub.asInterface(data.readStrongBinder());
-					this.checkLicense(_arg0, _arg1, _arg2);
-					return true;
-				}
-			}
-			return super.onTransact(code, data, reply, flags);
-		}
-		private static class Proxy implements com.google.android.vending.licensing.ILicensingService {
-			private android.os.IBinder mRemote;
-			Proxy(android.os.IBinder remote) {
-				mRemote = remote;
-			}
-			public android.os.IBinder asBinder() {
-				return mRemote;
-			}
-			public java.lang.String getInterfaceDescriptor() {
-				return DESCRIPTOR;
-			}
-			public void checkLicense(long nonce, java.lang.String packageName, com.google.android.vending.licensing.ILicenseResultListener listener) throws android.os.RemoteException {
-				android.os.Parcel _data = android.os.Parcel.obtain();
-				try {
-					_data.writeInterfaceToken(DESCRIPTOR);
-					_data.writeLong(nonce);
-					_data.writeString(packageName);
-					_data.writeStrongBinder((((listener != null)) ? (listener.asBinder()) : (null)));
-					mRemote.transact(Stub.TRANSACTION_checkLicense, _data, null, IBinder.FLAG_ONEWAY);
-				} finally {
-					_data.recycle();
-				}
-			}
-		}
-		static final int TRANSACTION_checkLicense = (IBinder.FIRST_CALL_TRANSACTION + 0);
-	}
-	public void checkLicense(long nonce, java.lang.String packageName, com.google.android.vending.licensing.ILicenseResultListener listener) throws android.os.RemoteException;
-}

+ 278 - 276
platform/android/java/src/com/google/android/vending/licensing/LicenseChecker.java

@@ -29,8 +29,8 @@ import android.os.RemoteException;
 import android.provider.Settings.Secure;
 import android.util.Log;
 
-import com.google.android.vending.licensing.ILicenseResultListener;
-import com.google.android.vending.licensing.ILicensingService;
+import com.android.vending.licensing.ILicenseResultListener;
+import com.android.vending.licensing.ILicensingService;
 import com.google.android.vending.licensing.util.Base64;
 import com.google.android.vending.licensing.util.Base64DecoderException;
 
@@ -58,73 +58,73 @@ import java.util.Set;
  * public key is obtainable from the publisher site.
  */
 public class LicenseChecker implements ServiceConnection {
-	private static final String TAG = "LicenseChecker";
+    private static final String TAG = "LicenseChecker";
 
-	private static final String KEY_FACTORY_ALGORITHM = "RSA";
+    private static final String KEY_FACTORY_ALGORITHM = "RSA";
 
-	// Timeout value (in milliseconds) for calls to service.
-	private static final int TIMEOUT_MS = 10 * 1000;
+    // Timeout value (in milliseconds) for calls to service.
+    private static final int TIMEOUT_MS = 10 * 1000;
 
-	private static final SecureRandom RANDOM = new SecureRandom();
-	private static final boolean DEBUG_LICENSE_ERROR = false;
+    private static final SecureRandom RANDOM = new SecureRandom();
+    private static final boolean DEBUG_LICENSE_ERROR = false;
 
-	private ILicensingService mService;
+    private ILicensingService mService;
 
-	private PublicKey mPublicKey;
-	private final Context mContext;
-	private final Policy mPolicy;
-	/**
+    private PublicKey mPublicKey;
+    private final Context mContext;
+    private final Policy mPolicy;
+    /**
      * A handler for running tasks on a background thread. We don't want license processing to block
      * the UI thread.
      */
-	private Handler mHandler;
-	private final String mPackageName;
-	private final String mVersionCode;
-	private final Set<LicenseValidator> mChecksInProgress = new HashSet<LicenseValidator>();
-	private final Queue<LicenseValidator> mPendingChecks = new LinkedList<LicenseValidator>();
+    private Handler mHandler;
+    private final String mPackageName;
+    private final String mVersionCode;
+    private final Set<LicenseValidator> mChecksInProgress = new HashSet<LicenseValidator>();
+    private final Queue<LicenseValidator> mPendingChecks = new LinkedList<LicenseValidator>();
 
-	/**
+    /**
      * @param context a Context
      * @param policy implementation of Policy
      * @param encodedPublicKey Base64-encoded RSA public key
      * @throws IllegalArgumentException if encodedPublicKey is invalid
      */
-	public LicenseChecker(Context context, Policy policy, String encodedPublicKey) {
-		mContext = context;
-		mPolicy = policy;
-		mPublicKey = generatePublicKey(encodedPublicKey);
-		mPackageName = mContext.getPackageName();
-		mVersionCode = getVersionCode(context, mPackageName);
-		HandlerThread handlerThread = new HandlerThread("background thread");
-		handlerThread.start();
-		mHandler = new Handler(handlerThread.getLooper());
-	}
-
-	/**
+    public LicenseChecker(Context context, Policy policy, String encodedPublicKey) {
+        mContext = context;
+        mPolicy = policy;
+        mPublicKey = generatePublicKey(encodedPublicKey);
+        mPackageName = mContext.getPackageName();
+        mVersionCode = getVersionCode(context, mPackageName);
+        HandlerThread handlerThread = new HandlerThread("background thread");
+        handlerThread.start();
+        mHandler = new Handler(handlerThread.getLooper());
+    }
+
+    /**
      * Generates a PublicKey instance from a string containing the Base64-encoded public key.
      *
      * @param encodedPublicKey Base64-encoded public key
      * @throws IllegalArgumentException if encodedPublicKey is invalid
      */
-	private static PublicKey generatePublicKey(String encodedPublicKey) {
-		try {
-			byte[] decodedKey = Base64.decode(encodedPublicKey);
-			KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM);
-
-			return keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey));
-		} catch (NoSuchAlgorithmException e) {
-			// This won't happen in an Android-compatible environment.
-			throw new RuntimeException(e);
-		} catch (Base64DecoderException e) {
-			Log.e(TAG, "Could not decode from Base64.");
-			throw new IllegalArgumentException(e);
-		} catch (InvalidKeySpecException e) {
-			Log.e(TAG, "Invalid key specification.");
-			throw new IllegalArgumentException(e);
-		}
-	}
-
-	/**
+    private static PublicKey generatePublicKey(String encodedPublicKey) {
+        try {
+            byte[] decodedKey = Base64.decode(encodedPublicKey);
+            KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM);
+
+            return keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey));
+        } catch (NoSuchAlgorithmException e) {
+            // This won't happen in an Android-compatible environment.
+            throw new RuntimeException(e);
+        } catch (Base64DecoderException e) {
+            Log.e(TAG, "Could not decode from Base64.");
+            throw new IllegalArgumentException(e);
+        } catch (InvalidKeySpecException e) {
+            Log.e(TAG, "Invalid key specification.");
+            throw new IllegalArgumentException(e);
+        }
+    }
+
+    /**
      * Checks if the user should have access to the app. Binds the service if necessary.
      * <p>
      * NOTE: This call uses a trivially obfuscated string (base64-encoded). For best security, we
@@ -136,221 +136,223 @@ public class LicenseChecker implements ServiceConnection {
      * 
      * @param callback
      */
-	public synchronized void checkAccess(LicenseCheckerCallback callback) {
-		// If we have a valid recent LICENSED response, we can skip asking
-		// Market.
-		if (mPolicy.allowAccess()) {
-			Log.i(TAG, "Using cached license response");
-			callback.allow(Policy.LICENSED);
-		} else {
-			LicenseValidator validator = new LicenseValidator(mPolicy, new NullDeviceLimiter(),
-					callback, generateNonce(), mPackageName, mVersionCode);
-
-			if (mService == null) {
-				Log.i(TAG, "Binding to licensing service.");
-				try {
-					boolean bindResult = mContext
-												 .bindService(
-														 new Intent(
-																 new String(
-																		 // Base64 encoded -
-																		 // com.android.vending.licensing.ILicensingService
-																		 // Consider encoding this in another way in your
-																		 // code to improve security
-																		 Base64.decode(
-																				 "Y29tLmFuZHJvaWQudmVuZGluZy5saWNlbnNpbmcuSUxpY2Vuc2luZ1NlcnZpY2U=")))
-																 // As of Android 5.0, implicit
-																 // Service Intents are no longer
-																 // allowed because it's not
-																 // possible for the user to
-																 // participate in disambiguating
-																 // them. This does mean we break
-																 // compatibility with Android
-																 // Cupcake devices with this
-																 // release, since setPackage was
-																 // added in Donut.
-																 .setPackage(
-																		 new String(
-																				 // Base64
-																				 // encoded -
-																				 // com.android.vending
-																				 Base64.decode(
-																						 "Y29tLmFuZHJvaWQudmVuZGluZw=="))),
-														 this, // ServiceConnection.
-														 Context.BIND_AUTO_CREATE);
-					if (bindResult) {
-						mPendingChecks.offer(validator);
-					} else {
-						Log.e(TAG, "Could not bind to service.");
-						handleServiceConnectionError(validator);
-					}
-				} catch (SecurityException e) {
-					callback.applicationError(LicenseCheckerCallback.ERROR_MISSING_PERMISSION);
-				} catch (Base64DecoderException e) {
-					e.printStackTrace();
-				}
-			} else {
-				mPendingChecks.offer(validator);
-				runChecks();
-			}
-		}
-	}
-
-	/**
+    public synchronized void checkAccess(LicenseCheckerCallback callback) {
+        // If we have a valid recent LICENSED response, we can skip asking
+        // Market.
+        if (mPolicy.allowAccess()) {
+            Log.i(TAG, "Using cached license response");
+            callback.allow(Policy.LICENSED);
+        } else {
+            LicenseValidator validator = new LicenseValidator(mPolicy, new NullDeviceLimiter(),
+                    callback, generateNonce(), mPackageName, mVersionCode);
+
+            if (mService == null) {
+                Log.i(TAG, "Binding to licensing service.");
+                try {
+                    boolean bindResult = mContext
+                            .bindService(
+                                    new Intent(
+                                            new String(
+                                                    // Base64 encoded -
+                                                    // com.android.vending.licensing.ILicensingService
+                                                    // Consider encoding this in another way in your
+                                                    // code to improve security
+                                                    Base64.decode(
+                                                            "Y29tLmFuZHJvaWQudmVuZGluZy5saWNlbnNpbmcuSUxpY2Vuc2luZ1NlcnZpY2U=")))
+                                                                    // As of Android 5.0, implicit
+                                                                    // Service Intents are no longer
+                                                                    // allowed because it's not
+                                                                    // possible for the user to
+                                                                    // participate in disambiguating
+                                                                    // them. This does mean we break
+                                                                    // compatibility with Android
+                                                                    // Cupcake devices with this
+                                                                    // release, since setPackage was
+                                                                    // added in Donut.
+                                                                    .setPackage(
+                                                                            new String(
+                                                                                    // Base64
+                                                                                    // encoded -
+                                                                                    // com.android.vending
+                                                                                    Base64.decode(
+                                                                                            "Y29tLmFuZHJvaWQudmVuZGluZw=="))),
+                                    this, // ServiceConnection.
+                                    Context.BIND_AUTO_CREATE);
+                    if (bindResult) {
+                        mPendingChecks.offer(validator);
+                    } else {
+                        Log.e(TAG, "Could not bind to service.");
+                        handleServiceConnectionError(validator);
+                    }
+                } catch (SecurityException e) {
+                    callback.applicationError(LicenseCheckerCallback.ERROR_MISSING_PERMISSION);
+                } catch (Base64DecoderException e) {
+                    e.printStackTrace();
+                }
+            } else {
+                mPendingChecks.offer(validator);
+                runChecks();
+            }
+        }
+    }
+
+    /**
      * Triggers the last deep link licensing URL returned from the server, which redirects users to a
      * page which enables them to gain access to the app. If no such URL is returned by the server, it
      * will go to the details page of the app in the Play Store.
      */
-	public void followLastLicensingUrl(Context context) {
-		String licensingUrl = mPolicy.getLicensingUrl();
-		if (licensingUrl == null) {
-			licensingUrl = "https://play.google.com/store/apps/details?id=" + context.getPackageName();
-		}
-		Intent marketIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(licensingUrl));
-		context.startActivity(marketIntent);
-	}
-
-	private void runChecks() {
-		LicenseValidator validator;
-		while ((validator = mPendingChecks.poll()) != null) {
-			try {
-				Log.i(TAG, "Calling checkLicense on service for " + validator.getPackageName());
-				mService.checkLicense(
-						validator.getNonce(), validator.getPackageName(),
-						new ResultListener(validator));
-				mChecksInProgress.add(validator);
-			} catch (RemoteException e) {
-				Log.w(TAG, "RemoteException in checkLicense call.", e);
-				handleServiceConnectionError(validator);
-			}
-		}
-	}
-
-	private synchronized void finishCheck(LicenseValidator validator) {
-		mChecksInProgress.remove(validator);
-		if (mChecksInProgress.isEmpty()) {
-			cleanupService();
-		}
-	}
-
-	private class ResultListener extends ILicenseResultListener.Stub {
-		private final LicenseValidator mValidator;
-		private Runnable mOnTimeout;
-
-		public ResultListener(LicenseValidator validator) {
-			mValidator = validator;
-			mOnTimeout = new Runnable() {
-				public void run() {
-					Log.i(TAG, "Check timed out.");
-					handleServiceConnectionError(mValidator);
-					finishCheck(mValidator);
-				}
-			};
-			startTimeout();
-		}
-
-		private static final int ERROR_CONTACTING_SERVER = 0x101;
-		private static final int ERROR_INVALID_PACKAGE_NAME = 0x102;
-		private static final int ERROR_NON_MATCHING_UID = 0x103;
-
-		// Runs in IPC thread pool. Post it to the Handler, so we can guarantee
-		// either this or the timeout runs.
-		public void verifyLicense(final int responseCode, final String signedData,
-				final String signature) {
-			mHandler.post(new Runnable() {
-				public void run() {
-					Log.i(TAG, "Received response.");
-					// Make sure it hasn't already timed out.
-					if (mChecksInProgress.contains(mValidator)) {
-						clearTimeout();
-						mValidator.verify(mPublicKey, responseCode, signedData, signature);
-						finishCheck(mValidator);
-					}
-					if (DEBUG_LICENSE_ERROR) {
-						boolean logResponse;
-						String stringError = null;
-						switch (responseCode) {
-							case ERROR_CONTACTING_SERVER:
-								logResponse = true;
-								stringError = "ERROR_CONTACTING_SERVER";
-								break;
-							case ERROR_INVALID_PACKAGE_NAME:
-								logResponse = true;
-								stringError = "ERROR_INVALID_PACKAGE_NAME";
-								break;
-							case ERROR_NON_MATCHING_UID:
-								logResponse = true;
-								stringError = "ERROR_NON_MATCHING_UID";
-								break;
-							default:
-								logResponse = false;
-						}
-
-						if (logResponse) {
-							String android_id = Secure.ANDROID_ID;
-							Date date = new Date();
-							Log.d(TAG, "Server Failure: " + stringError);
-							Log.d(TAG, "Android ID: " + android_id);
-							Log.d(TAG, "Time: " + date.toGMTString());
-						}
-					}
-				}
-			});
-		}
-
-		private void startTimeout() {
-			Log.i(TAG, "Start monitoring timeout.");
-			mHandler.postDelayed(mOnTimeout, TIMEOUT_MS);
-		}
-
-		private void clearTimeout() {
-			Log.i(TAG, "Clearing timeout.");
-			mHandler.removeCallbacks(mOnTimeout);
-		}
-	}
-
-	public synchronized void onServiceConnected(ComponentName name, IBinder service) {
-		mService = ILicensingService.Stub.asInterface(service);
-		runChecks();
-	}
-
-	public synchronized void onServiceDisconnected(ComponentName name) {
-		// Called when the connection with the service has been
-		// unexpectedly disconnected. That is, Market crashed.
-		// If there are any checks in progress, the timeouts will handle them.
-		Log.w(TAG, "Service unexpectedly disconnected.");
-		mService = null;
-	}
-
-	/**
+    public void followLastLicensingUrl(Context context) {
+        String licensingUrl = mPolicy.getLicensingUrl();
+        if (licensingUrl == null) {
+            licensingUrl = "https://play.google.com/store/apps/details?id=" + context.getPackageName();
+        }
+        Intent marketIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(licensingUrl));
+        context.startActivity(marketIntent);
+    }
+
+    private void runChecks() {
+        LicenseValidator validator;
+        while ((validator = mPendingChecks.poll()) != null) {
+            try {
+                Log.i(TAG, "Calling checkLicense on service for " + validator.getPackageName());
+                mService.checkLicense(
+                        validator.getNonce(), validator.getPackageName(),
+                        new ResultListener(validator));
+                mChecksInProgress.add(validator);
+            } catch (RemoteException e) {
+                Log.w(TAG, "RemoteException in checkLicense call.", e);
+                handleServiceConnectionError(validator);
+            }
+        }
+    }
+
+    private synchronized void finishCheck(LicenseValidator validator) {
+        mChecksInProgress.remove(validator);
+        if (mChecksInProgress.isEmpty()) {
+            cleanupService();
+        }
+    }
+
+    private class ResultListener extends ILicenseResultListener.Stub {
+        private final LicenseValidator mValidator;
+        private Runnable mOnTimeout;
+
+        public ResultListener(LicenseValidator validator) {
+            mValidator = validator;
+            mOnTimeout = new Runnable() {
+                public void run() {
+                    Log.i(TAG, "Check timed out.");
+                    handleServiceConnectionError(mValidator);
+                    finishCheck(mValidator);
+                }
+            };
+            startTimeout();
+        }
+
+        private static final int ERROR_CONTACTING_SERVER = 0x101;
+        private static final int ERROR_INVALID_PACKAGE_NAME = 0x102;
+        private static final int ERROR_NON_MATCHING_UID = 0x103;
+
+        // Runs in IPC thread pool. Post it to the Handler, so we can guarantee
+        // either this or the timeout runs.
+        public void verifyLicense(final int responseCode, final String signedData,
+                final String signature) {
+            mHandler.post(new Runnable() {
+                public void run() {
+                    Log.i(TAG, "Received response.");
+                    // Make sure it hasn't already timed out.
+                    if (mChecksInProgress.contains(mValidator)) {
+                        clearTimeout();
+                        mValidator.verify(mPublicKey, responseCode, signedData, signature);
+                        finishCheck(mValidator);
+                    }
+                    if (DEBUG_LICENSE_ERROR) {
+                        boolean logResponse;
+                        String stringError = null;
+                        switch (responseCode) {
+                            case ERROR_CONTACTING_SERVER:
+                                logResponse = true;
+                                stringError = "ERROR_CONTACTING_SERVER";
+                                break;
+                            case ERROR_INVALID_PACKAGE_NAME:
+                                logResponse = true;
+                                stringError = "ERROR_INVALID_PACKAGE_NAME";
+                                break;
+                            case ERROR_NON_MATCHING_UID:
+                                logResponse = true;
+                                stringError = "ERROR_NON_MATCHING_UID";
+                                break;
+                            default:
+                                logResponse = false;
+                        }
+
+                        if (logResponse) {
+                            String android_id = Secure.getString(mContext.getContentResolver(),
+                                    Secure.ANDROID_ID);
+                            Date date = new Date();
+                            Log.d(TAG, "Server Failure: " + stringError);
+                            Log.d(TAG, "Android ID: " + android_id);
+                            Log.d(TAG, "Time: " + date.toGMTString());
+                        }
+                    }
+
+                }
+            });
+        }
+
+        private void startTimeout() {
+            Log.i(TAG, "Start monitoring timeout.");
+            mHandler.postDelayed(mOnTimeout, TIMEOUT_MS);
+        }
+
+        private void clearTimeout() {
+            Log.i(TAG, "Clearing timeout.");
+            mHandler.removeCallbacks(mOnTimeout);
+        }
+    }
+
+    public synchronized void onServiceConnected(ComponentName name, IBinder service) {
+        mService = ILicensingService.Stub.asInterface(service);
+        runChecks();
+    }
+
+    public synchronized void onServiceDisconnected(ComponentName name) {
+        // Called when the connection with the service has been
+        // unexpectedly disconnected. That is, Market crashed.
+        // If there are any checks in progress, the timeouts will handle them.
+        Log.w(TAG, "Service unexpectedly disconnected.");
+        mService = null;
+    }
+
+    /**
      * Generates policy response for service connection errors, as a result of disconnections or
      * timeouts.
      */
-	private synchronized void handleServiceConnectionError(LicenseValidator validator) {
-		mPolicy.processServerResponse(Policy.RETRY, null);
-
-		if (mPolicy.allowAccess()) {
-			validator.getCallback().allow(Policy.RETRY);
-		} else {
-			validator.getCallback().dontAllow(Policy.RETRY);
-		}
-	}
-
-	/** Unbinds service if necessary and removes reference to it. */
-	private void cleanupService() {
-		if (mService != null) {
-			try {
-				mContext.unbindService(this);
-			} catch (IllegalArgumentException e) {
-				// Somehow we've already been unbound. This is a non-fatal
-				// error.
-				Log.e(TAG, "Unable to unbind from licensing service (already unbound)");
-			}
-			mService = null;
-		}
-	}
-
-	/**
+    private synchronized void handleServiceConnectionError(LicenseValidator validator) {
+        mPolicy.processServerResponse(Policy.RETRY, null);
+
+        if (mPolicy.allowAccess()) {
+            validator.getCallback().allow(Policy.RETRY);
+        } else {
+            validator.getCallback().dontAllow(Policy.RETRY);
+        }
+    }
+
+    /** Unbinds service if necessary and removes reference to it. */
+    private void cleanupService() {
+        if (mService != null) {
+            try {
+                mContext.unbindService(this);
+            } catch (IllegalArgumentException e) {
+                // Somehow we've already been unbound. This is a non-fatal
+                // error.
+                Log.e(TAG, "Unable to unbind from licensing service (already unbound)");
+            }
+            mService = null;
+        }
+    }
+
+    /**
      * Inform the library that the context is about to be destroyed, so that any open connections
      * can be cleaned up.
      * <p>
@@ -358,30 +360,30 @@ public class LicenseChecker implements ServiceConnection {
      * screen rotation if an Activity requests the license check or when the user exits the
      * application.
      */
-	public synchronized void onDestroy() {
-		cleanupService();
-		mHandler.getLooper().quit();
-	}
+    public synchronized void onDestroy() {
+        cleanupService();
+        mHandler.getLooper().quit();
+    }
 
-	/** Generates a nonce (number used once). */
-	private int generateNonce() {
-		return RANDOM.nextInt();
-	}
+    /** Generates a nonce (number used once). */
+    private int generateNonce() {
+        return RANDOM.nextInt();
+    }
 
-	/**
+    /**
      * Get version code for the application package name.
      *
      * @param context
      * @param packageName application package name
      * @return the version code or empty string if package not found
      */
-	private static String getVersionCode(Context context, String packageName) {
-		try {
-			return String.valueOf(
-					context.getPackageManager().getPackageInfo(packageName, 0).versionCode);
-		} catch (NameNotFoundException e) {
-			Log.e(TAG, "Package not found. could not get version code.");
-			return "";
-		}
-	}
+    private static String getVersionCode(Context context, String packageName) {
+        try {
+            return String.valueOf(
+                    context.getPackageManager().getPackageInfo(packageName, 0).versionCode);
+        } catch (NameNotFoundException e) {
+            Log.e(TAG, "Package not found. could not get version code.");
+            return "";
+        }
+    }
 }

+ 13 - 13
platform/android/java/src/com/google/android/vending/licensing/LicenseCheckerCallback.java

@@ -34,34 +34,34 @@ package com.google.android.vending.licensing;
  */
 public interface LicenseCheckerCallback {
 
-	/**
+    /**
      * Allow use. App should proceed as normal.
      *
      * @param reason Policy.LICENSED or Policy.RETRY typically. (although in
      *            theory the policy can return Policy.NOT_LICENSED here as well)
      */
-	public void allow(int reason);
+    public void allow(int reason);
 
-	/**
+    /**
      * Don't allow use. App should inform user and take appropriate action.
      *
      * @param reason Policy.NOT_LICENSED or Policy.RETRY. (although in theory
      *            the policy can return Policy.LICENSED here as well ---
      *            perhaps the call to the LVL took too long, for example)
      */
-	public void dontAllow(int reason);
+    public void dontAllow(int reason);
 
-	/** Application error codes. */
-	public static final int ERROR_INVALID_PACKAGE_NAME = 1;
-	public static final int ERROR_NON_MATCHING_UID = 2;
-	public static final int ERROR_NOT_MARKET_MANAGED = 3;
-	public static final int ERROR_CHECK_IN_PROGRESS = 4;
-	public static final int ERROR_INVALID_PUBLIC_KEY = 5;
-	public static final int ERROR_MISSING_PERMISSION = 6;
+    /** Application error codes. */
+    public static final int ERROR_INVALID_PACKAGE_NAME = 1;
+    public static final int ERROR_NON_MATCHING_UID = 2;
+    public static final int ERROR_NOT_MARKET_MANAGED = 3;
+    public static final int ERROR_CHECK_IN_PROGRESS = 4;
+    public static final int ERROR_INVALID_PUBLIC_KEY = 5;
+    public static final int ERROR_MISSING_PERMISSION = 6;
 
-	/**
+    /**
      * Error in application code. Caller did not call or set up license checker
      * correctly. Should be considered fatal.
      */
-	public void applicationError(int errorCode);
+    public void applicationError(int errorCode);
 }

+ 183 - 184
platform/android/java/src/com/google/android/vending/licensing/LicenseValidator.java

@@ -33,52 +33,52 @@ import java.security.SignatureException;
  * and process the response.
  */
 class LicenseValidator {
-	private static final String TAG = "LicenseValidator";
-
-	// Server response codes.
-	private static final int LICENSED = 0x0;
-	private static final int NOT_LICENSED = 0x1;
-	private static final int LICENSED_OLD_KEY = 0x2;
-	private static final int ERROR_NOT_MARKET_MANAGED = 0x3;
-	private static final int ERROR_SERVER_FAILURE = 0x4;
-	private static final int ERROR_OVER_QUOTA = 0x5;
-
-	private static final int ERROR_CONTACTING_SERVER = 0x101;
-	private static final int ERROR_INVALID_PACKAGE_NAME = 0x102;
-	private static final int ERROR_NON_MATCHING_UID = 0x103;
-
-	private final Policy mPolicy;
-	private final LicenseCheckerCallback mCallback;
-	private final int mNonce;
-	private final String mPackageName;
-	private final String mVersionCode;
-	private final DeviceLimiter mDeviceLimiter;
-
-	LicenseValidator(Policy policy, DeviceLimiter deviceLimiter, LicenseCheckerCallback callback,
-			int nonce, String packageName, String versionCode) {
-		mPolicy = policy;
-		mDeviceLimiter = deviceLimiter;
-		mCallback = callback;
-		mNonce = nonce;
-		mPackageName = packageName;
-		mVersionCode = versionCode;
-	}
-
-	public LicenseCheckerCallback getCallback() {
-		return mCallback;
-	}
-
-	public int getNonce() {
-		return mNonce;
-	}
-
-	public String getPackageName() {
-		return mPackageName;
-	}
-
-	private static final String SIGNATURE_ALGORITHM = "SHA1withRSA";
-
-	/**
+    private static final String TAG = "LicenseValidator";
+
+    // Server response codes.
+    private static final int LICENSED = 0x0;
+    private static final int NOT_LICENSED = 0x1;
+    private static final int LICENSED_OLD_KEY = 0x2;
+    private static final int ERROR_NOT_MARKET_MANAGED = 0x3;
+    private static final int ERROR_SERVER_FAILURE = 0x4;
+    private static final int ERROR_OVER_QUOTA = 0x5;
+
+    private static final int ERROR_CONTACTING_SERVER = 0x101;
+    private static final int ERROR_INVALID_PACKAGE_NAME = 0x102;
+    private static final int ERROR_NON_MATCHING_UID = 0x103;
+
+    private final Policy mPolicy;
+    private final LicenseCheckerCallback mCallback;
+    private final int mNonce;
+    private final String mPackageName;
+    private final String mVersionCode;
+    private final DeviceLimiter mDeviceLimiter;
+
+    LicenseValidator(Policy policy, DeviceLimiter deviceLimiter, LicenseCheckerCallback callback,
+             int nonce, String packageName, String versionCode) {
+        mPolicy = policy;
+        mDeviceLimiter = deviceLimiter;
+        mCallback = callback;
+        mNonce = nonce;
+        mPackageName = packageName;
+        mVersionCode = versionCode;
+    }
+
+    public LicenseCheckerCallback getCallback() {
+        return mCallback;
+    }
+
+    public int getNonce() {
+        return mNonce;
+    }
+
+    public String getPackageName() {
+        return mPackageName;
+    }
+
+    private static final String SIGNATURE_ALGORITHM = "SHA1withRSA";
+
+    /**
      * Verifies the response from server and calls appropriate callback method.
      *
      * @param publicKey public key associated with the developer account
@@ -86,147 +86,146 @@ class LicenseValidator {
      * @param signedData signed data from server
      * @param signature server signature
      */
-	public void verify(PublicKey publicKey, int responseCode, String signedData, String signature) {
-		String userId = null;
-		// Skip signature check for unsuccessful requests
-		ResponseData data = null;
-		if (responseCode == LICENSED || responseCode == NOT_LICENSED ||
-				responseCode == LICENSED_OLD_KEY) {
-			// Verify signature.
-			try {
-				if (TextUtils.isEmpty(signedData)) {
-					Log.e(TAG, "Signature verification failed: signedData is empty. "
-									   +
-									   "(Device not signed-in to any Google accounts?)");
-					handleInvalidResponse();
-					return;
-				}
-
-				Signature sig = Signature.getInstance(SIGNATURE_ALGORITHM);
-				sig.initVerify(publicKey);
-				sig.update(signedData.getBytes());
-
-				if (!sig.verify(Base64.decode(signature))) {
-					Log.e(TAG, "Signature verification failed.");
-					handleInvalidResponse();
-					return;
-				}
-			} catch (NoSuchAlgorithmException e) {
-				// This can't happen on an Android compatible device.
-				throw new RuntimeException(e);
-			} catch (InvalidKeyException e) {
-				handleApplicationError(LicenseCheckerCallback.ERROR_INVALID_PUBLIC_KEY);
-				return;
-			} catch (SignatureException e) {
-				throw new RuntimeException(e);
-			} catch (Base64DecoderException e) {
-				Log.e(TAG, "Could not Base64-decode signature.");
-				handleInvalidResponse();
-				return;
-			}
-
-			// Parse and validate response.
-			try {
-				data = ResponseData.parse(signedData);
-			} catch (IllegalArgumentException e) {
-				Log.e(TAG, "Could not parse response.");
-				handleInvalidResponse();
-				return;
-			}
-
-			if (data.responseCode != responseCode) {
-				Log.e(TAG, "Response codes don't match.");
-				handleInvalidResponse();
-				return;
-			}
-
-			if (data.nonce != mNonce) {
-				Log.e(TAG, "Nonce doesn't match.");
-				handleInvalidResponse();
-				return;
-			}
-
-			if (!data.packageName.equals(mPackageName)) {
-				Log.e(TAG, "Package name doesn't match.");
-				handleInvalidResponse();
-				return;
-			}
-
-			if (!data.versionCode.equals(mVersionCode)) {
-				Log.e(TAG, "Version codes don't match.");
-				handleInvalidResponse();
-				return;
-			}
-
-			// Application-specific user identifier.
-			userId = data.userId;
-			if (TextUtils.isEmpty(userId)) {
-				Log.e(TAG, "User identifier is empty.");
-				handleInvalidResponse();
-				return;
-			}
-		}
-
-		switch (responseCode) {
-			case LICENSED:
-			case LICENSED_OLD_KEY:
-				int limiterResponse = mDeviceLimiter.isDeviceAllowed(userId);
-				handleResponse(limiterResponse, data);
-				break;
-			case NOT_LICENSED:
-				handleResponse(Policy.NOT_LICENSED, data);
-				break;
-			case ERROR_CONTACTING_SERVER:
-				Log.w(TAG, "Error contacting licensing server.");
-				handleResponse(Policy.RETRY, data);
-				break;
-			case ERROR_SERVER_FAILURE:
-				Log.w(TAG, "An error has occurred on the licensing server.");
-				handleResponse(Policy.RETRY, data);
-				break;
-			case ERROR_OVER_QUOTA:
-				Log.w(TAG, "Licensing server is refusing to talk to this device, over quota.");
-				handleResponse(Policy.RETRY, data);
-				break;
-			case ERROR_INVALID_PACKAGE_NAME:
-				handleApplicationError(LicenseCheckerCallback.ERROR_INVALID_PACKAGE_NAME);
-				break;
-			case ERROR_NON_MATCHING_UID:
-				handleApplicationError(LicenseCheckerCallback.ERROR_NON_MATCHING_UID);
-				break;
-			case ERROR_NOT_MARKET_MANAGED:
-				handleApplicationError(LicenseCheckerCallback.ERROR_NOT_MARKET_MANAGED);
-				break;
-			default:
-				Log.e(TAG, "Unknown response code for license check.");
-				handleInvalidResponse();
-		}
-	}
-
-	/**
+    public void verify(PublicKey publicKey, int responseCode, String signedData, String signature) {
+        String userId = null;
+        // Skip signature check for unsuccessful requests
+        ResponseData data = null;
+        if (responseCode == LICENSED || responseCode == NOT_LICENSED ||
+                responseCode == LICENSED_OLD_KEY) {
+            // Verify signature.
+            try {
+                if (TextUtils.isEmpty(signedData)) {
+                    Log.e(TAG, "Signature verification failed: signedData is empty. " +
+                            "(Device not signed-in to any Google accounts?)");
+                    handleInvalidResponse();
+                    return;
+                }
+
+                Signature sig = Signature.getInstance(SIGNATURE_ALGORITHM);
+                sig.initVerify(publicKey);
+                sig.update(signedData.getBytes());
+
+                if (!sig.verify(Base64.decode(signature))) {
+                    Log.e(TAG, "Signature verification failed.");
+                    handleInvalidResponse();
+                    return;
+                }
+            } catch (NoSuchAlgorithmException e) {
+                // This can't happen on an Android compatible device.
+                throw new RuntimeException(e);
+            } catch (InvalidKeyException e) {
+                handleApplicationError(LicenseCheckerCallback.ERROR_INVALID_PUBLIC_KEY);
+                return;
+            } catch (SignatureException e) {
+                throw new RuntimeException(e);
+            } catch (Base64DecoderException e) {
+                Log.e(TAG, "Could not Base64-decode signature.");
+                handleInvalidResponse();
+                return;
+            }
+
+            // Parse and validate response.
+            try {
+                data = ResponseData.parse(signedData);
+            } catch (IllegalArgumentException e) {
+                Log.e(TAG, "Could not parse response.");
+                handleInvalidResponse();
+                return;
+            }
+
+            if (data.responseCode != responseCode) {
+                Log.e(TAG, "Response codes don't match.");
+                handleInvalidResponse();
+                return;
+            }
+
+            if (data.nonce != mNonce) {
+                Log.e(TAG, "Nonce doesn't match.");
+                handleInvalidResponse();
+                return;
+            }
+
+            if (!data.packageName.equals(mPackageName)) {
+                Log.e(TAG, "Package name doesn't match.");
+                handleInvalidResponse();
+                return;
+            }
+
+            if (!data.versionCode.equals(mVersionCode)) {
+                Log.e(TAG, "Version codes don't match.");
+                handleInvalidResponse();
+                return;
+            }
+
+            // Application-specific user identifier.
+            userId = data.userId;
+            if (TextUtils.isEmpty(userId)) {
+                Log.e(TAG, "User identifier is empty.");
+                handleInvalidResponse();
+                return;
+            }
+        }
+
+        switch (responseCode) {
+            case LICENSED:
+            case LICENSED_OLD_KEY:
+                int limiterResponse = mDeviceLimiter.isDeviceAllowed(userId);
+                handleResponse(limiterResponse, data);
+                break;
+            case NOT_LICENSED:
+                handleResponse(Policy.NOT_LICENSED, data);
+                break;
+            case ERROR_CONTACTING_SERVER:
+                Log.w(TAG, "Error contacting licensing server.");
+                handleResponse(Policy.RETRY, data);
+                break;
+            case ERROR_SERVER_FAILURE:
+                Log.w(TAG, "An error has occurred on the licensing server.");
+                handleResponse(Policy.RETRY, data);
+                break;
+            case ERROR_OVER_QUOTA:
+                Log.w(TAG, "Licensing server is refusing to talk to this device, over quota.");
+                handleResponse(Policy.RETRY, data);
+                break;
+            case ERROR_INVALID_PACKAGE_NAME:
+                handleApplicationError(LicenseCheckerCallback.ERROR_INVALID_PACKAGE_NAME);
+                break;
+            case ERROR_NON_MATCHING_UID:
+                handleApplicationError(LicenseCheckerCallback.ERROR_NON_MATCHING_UID);
+                break;
+            case ERROR_NOT_MARKET_MANAGED:
+                handleApplicationError(LicenseCheckerCallback.ERROR_NOT_MARKET_MANAGED);
+                break;
+            default:
+                Log.e(TAG, "Unknown response code for license check.");
+                handleInvalidResponse();
+        }
+    }
+
+    /**
      * Confers with policy and calls appropriate callback method.
      *
      * @param response
      * @param rawData
      */
-	private void handleResponse(int response, ResponseData rawData) {
-		// Update policy data and increment retry counter (if needed)
-		mPolicy.processServerResponse(response, rawData);
-
-		// Given everything we know, including cached data, ask the policy if we should grant
-		// access.
-		if (mPolicy.allowAccess()) {
-			mCallback.allow(response);
-		} else {
-			mCallback.dontAllow(response);
-		}
-	}
-
-	private void handleApplicationError(int code) {
-		mCallback.applicationError(code);
-	}
-
-	private void handleInvalidResponse() {
-		mCallback.dontAllow(Policy.NOT_LICENSED);
-	}
+    private void handleResponse(int response, ResponseData rawData) {
+        // Update policy data and increment retry counter (if needed)
+        mPolicy.processServerResponse(response, rawData);
+
+        // Given everything we know, including cached data, ask the policy if we should grant
+        // access.
+        if (mPolicy.allowAccess()) {
+            mCallback.allow(response);
+        } else {
+            mCallback.dontAllow(response);
+        }
+    }
+
+    private void handleApplicationError(int code) {
+        mCallback.applicationError(code);
+    }
+
+    private void handleInvalidResponse() {
+        mCallback.dontAllow(Policy.NOT_LICENSED);
+    }
 }

+ 3 - 3
platform/android/java/src/com/google/android/vending/licensing/NullDeviceLimiter.java

@@ -26,7 +26,7 @@ package com.google.android.vending.licensing;
  */
 public class NullDeviceLimiter implements DeviceLimiter {
 
-	public int isDeviceAllowed(String userId) {
-		return Policy.LICENSED;
-	}
+    public int isDeviceAllowed(String userId) {
+        return Policy.LICENSED;
+    }
 }

+ 4 - 4
platform/android/java/src/com/google/android/vending/licensing/Obfuscator.java

@@ -27,16 +27,16 @@ package com.google.android.vending.licensing;
  */
 public interface Obfuscator {
 
-	/**
+    /**
      * Obfuscate a string that is being stored into shared preferences.
      *
      * @param original The data that is to be obfuscated.
      * @param key The key for the data that is to be obfuscated.
      * @return A transformed version of the original data.
      */
-	String obfuscate(String original, String key);
+    String obfuscate(String original, String key);
 
-	/**
+    /**
      * Undo the transformation applied to data by the obfuscate() method.
      *
      * @param obfuscated The data that is to be un-obfuscated.
@@ -44,5 +44,5 @@ public interface Obfuscator {
      * @return The original data transformed by the obfuscate() method.
      * @throws ValidationException Optionally thrown if a data integrity check fails.
      */
-	String unobfuscate(String obfuscated, String key) throws ValidationException;
+    String unobfuscate(String obfuscated, String key) throws ValidationException;
 }

+ 13 - 13
platform/android/java/src/com/google/android/vending/licensing/Policy.java

@@ -22,27 +22,27 @@ package com.google.android.vending.licensing;
  */
 public interface Policy {
 
-	/**
+    /**
      * Change these values to make it more difficult for tools to automatically
      * strip LVL protection from your APK.
      */
 
-	/**
+    /**
      * LICENSED means that the server returned back a valid license response
      */
-	public static final int LICENSED = 0x0100;
-	/**
+    public static final int LICENSED = 0x0100;
+    /**
      * NOT_LICENSED means that the server returned back a valid license response
      * that indicated that the user definitively is not licensed
      */
-	public static final int NOT_LICENSED = 0x0231;
-	/**
+    public static final int NOT_LICENSED = 0x0231;
+    /**
      * RETRY means that the license response was unable to be determined ---
      * perhaps as a result of faulty networking
      */
-	public static final int RETRY = 0x0123;
+    public static final int RETRY = 0x0123;
 
-	/**
+    /**
      * Provide results from contact with the license server. Retry counts are
      * incremented if the current value of response is RETRY. Results will be
      * used for any future policy decisions.
@@ -50,16 +50,16 @@ public interface Policy {
      * @param response the result from validating the server response
      * @param rawData the raw server response data, can be null for RETRY
      */
-	void processServerResponse(int response, ResponseData rawData);
+    void processServerResponse(int response, ResponseData rawData);
 
-	/**
+    /**
      * Check if the user should be allowed access to the application.
      */
-	boolean allowAccess();
+    boolean allowAccess();
 
-	/**
+    /**
      * Gets the licensing URL returned by the server that can enable access for unlicensed apps (e.g.
      * buy app on the Play Store).
      */
-	String getLicensingUrl();
+    String getLicensingUrl();
 }

+ 43 - 41
platform/android/java/src/com/google/android/vending/licensing/PreferenceObfuscator.java

@@ -24,55 +24,57 @@ import android.util.Log;
  */
 public class PreferenceObfuscator {
 
-	private static final String TAG = "PreferenceObfuscator";
+    private static final String TAG = "PreferenceObfuscator";
 
-	private final SharedPreferences mPreferences;
-	private final Obfuscator mObfuscator;
-	private SharedPreferences.Editor mEditor;
+    private final SharedPreferences mPreferences;
+    private final Obfuscator mObfuscator;
+    private SharedPreferences.Editor mEditor;
 
-	/**
+    /**
      * Constructor.
      *
      * @param sp A SharedPreferences instance provided by the system.
      * @param o The Obfuscator to use when reading or writing data.
      */
-	public PreferenceObfuscator(SharedPreferences sp, Obfuscator o) {
-		mPreferences = sp;
-		mObfuscator = o;
-		mEditor = null;
-	}
+    public PreferenceObfuscator(SharedPreferences sp, Obfuscator o) {
+        mPreferences = sp;
+        mObfuscator = o;
+        mEditor = null;
+    }
 
-	public void putString(String key, String value) {
-		if (mEditor == null) {
-			mEditor = mPreferences.edit();
-			mEditor.apply();
-		}
-		String obfuscatedValue = mObfuscator.obfuscate(value, key);
-		mEditor.putString(key, obfuscatedValue);
-	}
+    public void putString(String key, String value) {
+        if (mEditor == null) {
+            mEditor = mPreferences.edit();
+            // -- GODOT start --
+            mEditor.apply();
+            // -- GODOT end --
+        }
+        String obfuscatedValue = mObfuscator.obfuscate(value, key);
+        mEditor.putString(key, obfuscatedValue);
+    }
 
-	public String getString(String key, String defValue) {
-		String result;
-		String value = mPreferences.getString(key, null);
-		if (value != null) {
-			try {
-				result = mObfuscator.unobfuscate(value, key);
-			} catch (ValidationException e) {
-				// Unable to unobfuscate, data corrupt or tampered
-				Log.w(TAG, "Validation error while reading preference: " + key);
-				result = defValue;
-			}
-		} else {
-			// Preference not found
-			result = defValue;
-		}
-		return result;
-	}
+    public String getString(String key, String defValue) {
+        String result;
+        String value = mPreferences.getString(key, null);
+        if (value != null) {
+            try {
+                result = mObfuscator.unobfuscate(value, key);
+            } catch (ValidationException e) {
+                // Unable to unobfuscate, data corrupt or tampered
+                Log.w(TAG, "Validation error while reading preference: " + key);
+                result = defValue;
+            }
+        } else {
+            // Preference not found
+            result = defValue;
+        }
+        return result;
+    }
 
-	public void commit() {
-		if (mEditor != null) {
-			mEditor.commit();
-			mEditor = null;
-		}
-	}
+    public void commit() {
+        if (mEditor != null) {
+            mEditor.commit();
+            mEditor = null;
+        }
+    }
 }

+ 42 - 41
platform/android/java/src/com/google/android/vending/licensing/ResponseData.java

@@ -25,56 +25,57 @@ import java.util.regex.Pattern;
  */
 public class ResponseData {
 
-	public int responseCode;
-	public int nonce;
-	public String packageName;
-	public String versionCode;
-	public String userId;
-	public long timestamp;
-	/** Response-specific data. */
-	public String extra;
+    public int responseCode;
+    public int nonce;
+    public String packageName;
+    public String versionCode;
+    public String userId;
+    public long timestamp;
+    /** Response-specific data. */
+    public String extra;
 
-	/**
+    /**
      * Parses response string into ResponseData.
      *
      * @param responseData response data string
      * @throws IllegalArgumentException upon parsing error
      * @return ResponseData object
      */
-	public static ResponseData parse(String responseData) {
-		// Must parse out main response data and response-specific data.
-		int index = responseData.indexOf(':');
-		String mainData, extraData;
-		if (-1 == index) {
-			mainData = responseData;
-			extraData = "";
-		} else {
-			mainData = responseData.substring(0, index);
-			extraData = index >= responseData.length() ? "" : responseData.substring(index + 1);
-		}
+    public static ResponseData parse(String responseData) {
+        // Must parse out main response data and response-specific data.
+        int index = responseData.indexOf(':');
+        String mainData, extraData;
+        if (-1 == index) {
+            mainData = responseData;
+            extraData = "";
+        } else {
+            mainData = responseData.substring(0, index);
+            extraData = index >= responseData.length() ? "" : responseData.substring(index + 1);
+        }
 
-		String[] fields = TextUtils.split(mainData, Pattern.quote("|"));
-		if (fields.length < 6) {
-			throw new IllegalArgumentException("Wrong number of fields.");
-		}
+        String[] fields = TextUtils.split(mainData, Pattern.quote("|"));
+        if (fields.length < 6) {
+            throw new IllegalArgumentException("Wrong number of fields.");
+        }
 
-		ResponseData data = new ResponseData();
-		data.extra = extraData;
-		data.responseCode = Integer.parseInt(fields[0]);
-		data.nonce = Integer.parseInt(fields[1]);
-		data.packageName = fields[2];
-		data.versionCode = fields[3];
-		// Application-specific user identifier.
-		data.userId = fields[4];
-		data.timestamp = Long.parseLong(fields[5]);
+        ResponseData data = new ResponseData();
+        data.extra = extraData;
+        data.responseCode = Integer.parseInt(fields[0]);
+        data.nonce = Integer.parseInt(fields[1]);
+        data.packageName = fields[2];
+        data.versionCode = fields[3];
+        // Application-specific user identifier.
+        data.userId = fields[4];
+        data.timestamp = Long.parseLong(fields[5]);
 
-		return data;
-	}
+        return data;
+    }
 
-	@Override
-	public String toString() {
-		return TextUtils.join("|", new Object[] {
-										   responseCode, nonce, packageName, versionCode,
-										   userId, timestamp });
-	}
+    @Override
+    public String toString() {
+        return TextUtils.join("|", new Object[] {
+                responseCode, nonce, packageName, versionCode,
+                userId, timestamp
+        });
+    }
 }

+ 168 - 167
platform/android/java/src/com/google/android/vending/licensing/ServerManagedPolicy.java

@@ -43,49 +43,49 @@ import com.google.android.vending.licensing.util.URIQueryDecoder;
  */
 public class ServerManagedPolicy implements Policy {
 
-	private static final String TAG = "ServerManagedPolicy";
-	private static final String PREFS_FILE = "com.google.android.vending.licensing.ServerManagedPolicy";
-	private static final String PREF_LAST_RESPONSE = "lastResponse";
-	private static final String PREF_VALIDITY_TIMESTAMP = "validityTimestamp";
-	private static final String PREF_RETRY_UNTIL = "retryUntil";
-	private static final String PREF_MAX_RETRIES = "maxRetries";
-	private static final String PREF_RETRY_COUNT = "retryCount";
-	private static final String PREF_LICENSING_URL = "licensingUrl";
-	private static final String DEFAULT_VALIDITY_TIMESTAMP = "0";
-	private static final String DEFAULT_RETRY_UNTIL = "0";
-	private static final String DEFAULT_MAX_RETRIES = "0";
-	private static final String DEFAULT_RETRY_COUNT = "0";
+    private static final String TAG = "ServerManagedPolicy";
+    private static final String PREFS_FILE = "com.google.android.vending.licensing.ServerManagedPolicy";
+    private static final String PREF_LAST_RESPONSE = "lastResponse";
+    private static final String PREF_VALIDITY_TIMESTAMP = "validityTimestamp";
+    private static final String PREF_RETRY_UNTIL = "retryUntil";
+    private static final String PREF_MAX_RETRIES = "maxRetries";
+    private static final String PREF_RETRY_COUNT = "retryCount";
+    private static final String PREF_LICENSING_URL = "licensingUrl";
+    private static final String DEFAULT_VALIDITY_TIMESTAMP = "0";
+    private static final String DEFAULT_RETRY_UNTIL = "0";
+    private static final String DEFAULT_MAX_RETRIES = "0";
+    private static final String DEFAULT_RETRY_COUNT = "0";
 
-	private static final long MILLIS_PER_MINUTE = 60 * 1000;
+    private static final long MILLIS_PER_MINUTE = 60 * 1000;
 
-	private long mValidityTimestamp;
-	private long mRetryUntil;
-	private long mMaxRetries;
-	private long mRetryCount;
-	private long mLastResponseTime = 0;
-	private int mLastResponse;
-	private String mLicensingUrl;
-	private PreferenceObfuscator mPreferences;
+    private long mValidityTimestamp;
+    private long mRetryUntil;
+    private long mMaxRetries;
+    private long mRetryCount;
+    private long mLastResponseTime = 0;
+    private int mLastResponse;
+    private String mLicensingUrl;
+    private PreferenceObfuscator mPreferences;
 
-	/**
+    /**
      * @param context The context for the current application
      * @param obfuscator An obfuscator to be used with preferences.
      */
-	public ServerManagedPolicy(Context context, Obfuscator obfuscator) {
-		// Import old values
-		SharedPreferences sp = context.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE);
-		mPreferences = new PreferenceObfuscator(sp, obfuscator);
-		mLastResponse = Integer.parseInt(
-				mPreferences.getString(PREF_LAST_RESPONSE, Integer.toString(Policy.RETRY)));
-		mValidityTimestamp = Long.parseLong(mPreferences.getString(PREF_VALIDITY_TIMESTAMP,
-				DEFAULT_VALIDITY_TIMESTAMP));
-		mRetryUntil = Long.parseLong(mPreferences.getString(PREF_RETRY_UNTIL, DEFAULT_RETRY_UNTIL));
-		mMaxRetries = Long.parseLong(mPreferences.getString(PREF_MAX_RETRIES, DEFAULT_MAX_RETRIES));
-		mRetryCount = Long.parseLong(mPreferences.getString(PREF_RETRY_COUNT, DEFAULT_RETRY_COUNT));
-		mLicensingUrl = mPreferences.getString(PREF_LICENSING_URL, null);
-	}
+    public ServerManagedPolicy(Context context, Obfuscator obfuscator) {
+        // Import old values
+        SharedPreferences sp = context.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE);
+        mPreferences = new PreferenceObfuscator(sp, obfuscator);
+        mLastResponse = Integer.parseInt(
+            mPreferences.getString(PREF_LAST_RESPONSE, Integer.toString(Policy.RETRY)));
+        mValidityTimestamp = Long.parseLong(mPreferences.getString(PREF_VALIDITY_TIMESTAMP,
+                DEFAULT_VALIDITY_TIMESTAMP));
+        mRetryUntil = Long.parseLong(mPreferences.getString(PREF_RETRY_UNTIL, DEFAULT_RETRY_UNTIL));
+        mMaxRetries = Long.parseLong(mPreferences.getString(PREF_MAX_RETRIES, DEFAULT_MAX_RETRIES));
+        mRetryCount = Long.parseLong(mPreferences.getString(PREF_RETRY_COUNT, DEFAULT_RETRY_COUNT));
+        mLicensingUrl = mPreferences.getString(PREF_LICENSING_URL, null);
+    }
 
-	/**
+    /**
      * Process a new response from the license server.
      * <p>
      * This data will be used for computing future policy decisions. The
@@ -102,159 +102,159 @@ public class ServerManagedPolicy implements Policy {
      * @param response the result from validating the server response
      * @param rawData the raw server response data
      */
-	public void processServerResponse(int response, ResponseData rawData) {
+    public void processServerResponse(int response, ResponseData rawData) {
 
-		// Update retry counter
-		if (response != Policy.RETRY) {
-			setRetryCount(0);
-		} else {
-			setRetryCount(mRetryCount + 1);
-		}
+        // Update retry counter
+        if (response != Policy.RETRY) {
+            setRetryCount(0);
+        } else {
+            setRetryCount(mRetryCount + 1);
+        }
 
-		// Update server policy data
-		Map<String, String> extras = decodeExtras(rawData);
-		if (response == Policy.LICENSED) {
-			mLastResponse = response;
-			// Reset the licensing URL since it is only applicable for NOT_LICENSED responses.
-			setLicensingUrl(null);
-			setValidityTimestamp(extras.get("VT"));
-			setRetryUntil(extras.get("GT"));
-			setMaxRetries(extras.get("GR"));
-		} else if (response == Policy.NOT_LICENSED) {
-			// Clear out stale retry params
-			setValidityTimestamp(DEFAULT_VALIDITY_TIMESTAMP);
-			setRetryUntil(DEFAULT_RETRY_UNTIL);
-			setMaxRetries(DEFAULT_MAX_RETRIES);
-			// Update the licensing URL
-			setLicensingUrl(extras.get("LU"));
-		}
+        // Update server policy data
+        Map<String, String> extras = decodeExtras(rawData);
+        if (response == Policy.LICENSED) {
+            mLastResponse = response;
+            // Reset the licensing URL since it is only applicable for NOT_LICENSED responses.
+            setLicensingUrl(null);
+            setValidityTimestamp(extras.get("VT"));
+            setRetryUntil(extras.get("GT"));
+            setMaxRetries(extras.get("GR"));
+        } else if (response == Policy.NOT_LICENSED) {
+            // Clear out stale retry params
+            setValidityTimestamp(DEFAULT_VALIDITY_TIMESTAMP);
+            setRetryUntil(DEFAULT_RETRY_UNTIL);
+            setMaxRetries(DEFAULT_MAX_RETRIES);
+            // Update the licensing URL
+            setLicensingUrl(extras.get("LU"));
+        }
 
-		setLastResponse(response);
-		mPreferences.commit();
-	}
+        setLastResponse(response);
+        mPreferences.commit();
+    }
 
-	/**
+    /**
      * Set the last license response received from the server and add to
      * preferences. You must manually call PreferenceObfuscator.commit() to
      * commit these changes to disk.
      *
      * @param l the response
      */
-	private void setLastResponse(int l) {
-		mLastResponseTime = System.currentTimeMillis();
-		mLastResponse = l;
-		mPreferences.putString(PREF_LAST_RESPONSE, Integer.toString(l));
-	}
+    private void setLastResponse(int l) {
+        mLastResponseTime = System.currentTimeMillis();
+        mLastResponse = l;
+        mPreferences.putString(PREF_LAST_RESPONSE, Integer.toString(l));
+    }
 
-	/**
+    /**
      * Set the current retry count and add to preferences. You must manually
      * call PreferenceObfuscator.commit() to commit these changes to disk.
      *
      * @param c the new retry count
      */
-	private void setRetryCount(long c) {
-		mRetryCount = c;
-		mPreferences.putString(PREF_RETRY_COUNT, Long.toString(c));
-	}
+    private void setRetryCount(long c) {
+        mRetryCount = c;
+        mPreferences.putString(PREF_RETRY_COUNT, Long.toString(c));
+    }
 
-	public long getRetryCount() {
-		return mRetryCount;
-	}
+    public long getRetryCount() {
+        return mRetryCount;
+    }
 
-	/**
+    /**
      * Set the last validity timestamp (VT) received from the server and add to
      * preferences. You must manually call PreferenceObfuscator.commit() to
      * commit these changes to disk.
      *
      * @param validityTimestamp the VT string received
      */
-	private void setValidityTimestamp(String validityTimestamp) {
-		Long lValidityTimestamp;
-		try {
-			lValidityTimestamp = Long.parseLong(validityTimestamp);
-		} catch (NumberFormatException e) {
-			// No response or not parsable, expire in one minute.
-			Log.w(TAG, "License validity timestamp (VT) missing, caching for a minute");
-			lValidityTimestamp = System.currentTimeMillis() + MILLIS_PER_MINUTE;
-			validityTimestamp = Long.toString(lValidityTimestamp);
-		}
+    private void setValidityTimestamp(String validityTimestamp) {
+        Long lValidityTimestamp;
+        try {
+            lValidityTimestamp = Long.parseLong(validityTimestamp);
+        } catch (NumberFormatException e) {
+            // No response or not parsable, expire in one minute.
+            Log.w(TAG, "License validity timestamp (VT) missing, caching for a minute");
+            lValidityTimestamp = System.currentTimeMillis() + MILLIS_PER_MINUTE;
+            validityTimestamp = Long.toString(lValidityTimestamp);
+        }
 
-		mValidityTimestamp = lValidityTimestamp;
-		mPreferences.putString(PREF_VALIDITY_TIMESTAMP, validityTimestamp);
-	}
+        mValidityTimestamp = lValidityTimestamp;
+        mPreferences.putString(PREF_VALIDITY_TIMESTAMP, validityTimestamp);
+    }
 
-	public long getValidityTimestamp() {
-		return mValidityTimestamp;
-	}
+    public long getValidityTimestamp() {
+        return mValidityTimestamp;
+    }
 
-	/**
+    /**
      * Set the retry until timestamp (GT) received from the server and add to
      * preferences. You must manually call PreferenceObfuscator.commit() to
      * commit these changes to disk.
      *
      * @param retryUntil the GT string received
      */
-	private void setRetryUntil(String retryUntil) {
-		Long lRetryUntil;
-		try {
-			lRetryUntil = Long.parseLong(retryUntil);
-		} catch (NumberFormatException e) {
-			// No response or not parsable, expire immediately
-			Log.w(TAG, "License retry timestamp (GT) missing, grace period disabled");
-			retryUntil = "0";
-			lRetryUntil = 0l;
-		}
+    private void setRetryUntil(String retryUntil) {
+        Long lRetryUntil;
+        try {
+            lRetryUntil = Long.parseLong(retryUntil);
+        } catch (NumberFormatException e) {
+            // No response or not parsable, expire immediately
+            Log.w(TAG, "License retry timestamp (GT) missing, grace period disabled");
+            retryUntil = "0";
+            lRetryUntil = 0l;
+        }
 
-		mRetryUntil = lRetryUntil;
-		mPreferences.putString(PREF_RETRY_UNTIL, retryUntil);
-	}
+        mRetryUntil = lRetryUntil;
+        mPreferences.putString(PREF_RETRY_UNTIL, retryUntil);
+    }
 
-	public long getRetryUntil() {
-		return mRetryUntil;
-	}
+    public long getRetryUntil() {
+      return mRetryUntil;
+    }
 
-	/**
+    /**
      * Set the max retries value (GR) as received from the server and add to
      * preferences. You must manually call PreferenceObfuscator.commit() to
      * commit these changes to disk.
      *
      * @param maxRetries the GR string received
      */
-	private void setMaxRetries(String maxRetries) {
-		Long lMaxRetries;
-		try {
-			lMaxRetries = Long.parseLong(maxRetries);
-		} catch (NumberFormatException e) {
-			// No response or not parsable, expire immediately
-			Log.w(TAG, "Licence retry count (GR) missing, grace period disabled");
-			maxRetries = "0";
-			lMaxRetries = 0l;
-		}
+    private void setMaxRetries(String maxRetries) {
+        Long lMaxRetries;
+        try {
+            lMaxRetries = Long.parseLong(maxRetries);
+        } catch (NumberFormatException e) {
+            // No response or not parsable, expire immediately
+            Log.w(TAG, "Licence retry count (GR) missing, grace period disabled");
+            maxRetries = "0";
+            lMaxRetries = 0l;
+        }
 
-		mMaxRetries = lMaxRetries;
-		mPreferences.putString(PREF_MAX_RETRIES, maxRetries);
-	}
+        mMaxRetries = lMaxRetries;
+        mPreferences.putString(PREF_MAX_RETRIES, maxRetries);
+    }
 
-	public long getMaxRetries() {
-		return mMaxRetries;
-	}
+    public long getMaxRetries() {
+        return mMaxRetries;
+    }
 
-	/**
+    /**
      * Set the license URL value (LU) as received from the server and add to preferences. You must
      * manually call PreferenceObfuscator.commit() to commit these changes to disk.
      *
      * @param url the LU string received
      */
-	private void setLicensingUrl(String url) {
-		mLicensingUrl = url;
-		mPreferences.putString(PREF_LICENSING_URL, url);
-	}
+    private void setLicensingUrl(String url) {
+        mLicensingUrl = url;
+        mPreferences.putString(PREF_LICENSING_URL, url);
+    }
 
-	public String getLicensingUrl() {
-		return mLicensingUrl;
-	}
+    public String getLicensingUrl() {
+        return mLicensingUrl;
+    }
 
-	/**
+    /**
      * {@inheritDoc}
      *
      * This implementation allows access if either:<br>
@@ -264,36 +264,37 @@ public class ServerManagedPolicy implements Policy {
      * the RETRY count or in the RETRY period.
      * </ol>
      */
-	public boolean allowAccess() {
-		long ts = System.currentTimeMillis();
-		if (mLastResponse == Policy.LICENSED) {
-			// Check if the LICENSED response occurred within the validity timeout.
-			if (ts <= mValidityTimestamp) {
-				// Cached LICENSED response is still valid.
-				return true;
-			}
-		} else if (mLastResponse == Policy.RETRY &&
-				   ts < mLastResponseTime + MILLIS_PER_MINUTE) {
-			// Only allow access if we are within the retry period or we haven't used up our
-			// max retries.
-			return (ts <= mRetryUntil || mRetryCount <= mMaxRetries);
-		}
-		return false;
-	}
+    public boolean allowAccess() {
+        long ts = System.currentTimeMillis();
+        if (mLastResponse == Policy.LICENSED) {
+            // Check if the LICENSED response occurred within the validity timeout.
+            if (ts <= mValidityTimestamp) {
+                // Cached LICENSED response is still valid.
+                return true;
+            }
+        } else if (mLastResponse == Policy.RETRY &&
+                   ts < mLastResponseTime + MILLIS_PER_MINUTE) {
+            // Only allow access if we are within the retry period or we haven't used up our
+            // max retries.
+            return (ts <= mRetryUntil || mRetryCount <= mMaxRetries);
+        }
+        return false;
+    }
 
-	private Map<String, String> decodeExtras(
-			com.google.android.vending.licensing.ResponseData rawData) {
-		Map<String, String> results = new HashMap<String, String>();
-		if (rawData == null) {
-			return results;
-		}
+    private Map<String, String> decodeExtras(
+        com.google.android.vending.licensing.ResponseData rawData) {
+        Map<String, String> results = new HashMap<String, String>();
+        if (rawData == null) {
+            return results;
+        }
+
+        try {
+            URI rawExtras = new URI("?" + rawData.extra);
+            URIQueryDecoder.DecodeQuery(rawExtras, results);
+        } catch (URISyntaxException e) {
+            Log.w(TAG, "Invalid syntax error while decoding extras data from server.");
+        }
+        return results;
+    }
 
-		try {
-			URI rawExtras = new URI("?" + rawData.extra);
-			URIQueryDecoder.DecodeQuery(rawExtras, results);
-		} catch (URISyntaxException e) {
-			Log.w(TAG, "Invalid syntax error while decoding extras data from server.");
-		}
-		return results;
-	}
 }

+ 38 - 37
platform/android/java/src/com/google/android/vending/licensing/StrictPolicy.java

@@ -38,18 +38,18 @@ import java.util.Map;
  */
 public class StrictPolicy implements Policy {
 
-	private static final String TAG = "StrictPolicy";
+    private static final String TAG = "StrictPolicy";
 
-	private int mLastResponse;
-	private String mLicensingUrl;
+    private int mLastResponse;
+    private String mLicensingUrl;
 
-	public StrictPolicy() {
-		// Set default policy. This will force the application to check the policy on launch.
-		mLastResponse = Policy.RETRY;
-		mLicensingUrl = null;
-	}
+    public StrictPolicy() {
+        // Set default policy. This will force the application to check the policy on launch.
+        mLastResponse = Policy.RETRY;
+        mLicensingUrl = null;
+    }
 
-	/**
+    /**
      * Process a new response from the license server. Since we aren't
      * performing any caching, this equates to reading the LicenseResponse.
      * Any cache-related ResponseData is ignored, but the licensing URL
@@ -58,42 +58,43 @@ public class StrictPolicy implements Policy {
      * @param response the result from validating the server response
      * @param rawData the raw server response data
      */
-	public void processServerResponse(int response, ResponseData rawData) {
-		mLastResponse = response;
+    public void processServerResponse(int response, ResponseData rawData) {
+        mLastResponse = response;
 
-		if (response == Policy.NOT_LICENSED) {
-			Map<String, String> extras = decodeExtras(rawData);
-			mLicensingUrl = extras.get("LU");
-		}
-	}
+        if (response == Policy.NOT_LICENSED) {
+            Map<String, String> extras = decodeExtras(rawData);
+            mLicensingUrl = extras.get("LU");
+        }
+    }
 
-	/**
+    /**
      * {@inheritDoc}
      *
      * This implementation allows access if and only if a LICENSED response
      * was received the last time the server was contacted.
      */
-	public boolean allowAccess() {
-		return (mLastResponse == Policy.LICENSED);
-	}
+    public boolean allowAccess() {
+        return (mLastResponse == Policy.LICENSED);
+    }
 
-	public String getLicensingUrl() {
-		return mLicensingUrl;
-	}
+    public String getLicensingUrl() {
+        return mLicensingUrl;
+    }
 
-	private Map<String, String> decodeExtras(
-			com.google.android.vending.licensing.ResponseData rawData) {
-		Map<String, String> results = new HashMap<String, String>();
-		if (rawData == null) {
-			return results;
-		}
+    private Map<String, String> decodeExtras(
+        com.google.android.vending.licensing.ResponseData rawData) {
+        Map<String, String> results = new HashMap<String, String>();
+        if (rawData == null) {
+            return results;
+        }
+
+        try {
+            URI rawExtras = new URI("?" + rawData.extra);
+            URIQueryDecoder.DecodeQuery(rawExtras, results);
+        } catch (URISyntaxException e) {
+            Log.w(TAG, "Invalid syntax error while decoding extras data from server.");
+        }
+        return results;
+    }
 
-		try {
-			URI rawExtras = new URI("?" + rawData.extra);
-			URIQueryDecoder.DecodeQuery(rawExtras, results);
-		} catch (URISyntaxException e) {
-			Log.w(TAG, "Invalid syntax error while decoding extras data from server.");
-		}
-		return results;
-	}
 }

+ 7 - 7
platform/android/java/src/com/google/android/vending/licensing/ValidationException.java

@@ -21,13 +21,13 @@ package com.google.android.vending.licensing;
  * {@link Obfuscator}.}
  */
 public class ValidationException extends Exception {
-	public ValidationException() {
-		super();
-	}
+    public ValidationException() {
+      super();
+    }
 
-	public ValidationException(String s) {
-		super(s);
-	}
+    public ValidationException(String s) {
+      super(s);
+    }
 
-	private static final long serialVersionUID = 1L;
+    private static final long serialVersionUID = 1L;
 }

+ 363 - 341
platform/android/java/src/com/google/android/vending/licensing/util/Base64.java

@@ -31,7 +31,9 @@ package com.google.android.vending.licensing.util;
  * @version 1.3
  */
 
+// -- GODOT start --
 import com.godot.game.BuildConfig;
+// -- GODOT end --
 
 /**
  * Base64 converter class. This code is not a full-blown MIME encoder;
@@ -41,79 +43,80 @@ import com.godot.game.BuildConfig;
  * class.
  */
 public class Base64 {
-	/** Specify encoding (value is {@code true}). */
-	public final static boolean ENCODE = true;
+  /** Specify encoding (value is {@code true}). */
+  public final static boolean ENCODE = true;
 
-	/** Specify decoding (value is {@code false}). */
-	public final static boolean DECODE = false;
+  /** Specify decoding (value is {@code false}). */
+  public final static boolean DECODE = false;
 
-	/** The equals sign (=) as a byte. */
-	private final static byte EQUALS_SIGN = (byte)'=';
+  /** The equals sign (=) as a byte. */
+  private final static byte EQUALS_SIGN = (byte) '=';
 
-	/** The new line character (\n) as a byte. */
-	private final static byte NEW_LINE = (byte)'\n';
+  /** The new line character (\n) as a byte. */
+  private final static byte NEW_LINE = (byte) '\n';
 
-	/**
+  /**
    * The 64 valid Base64 values.
    */
-	private final static byte[] ALPHABET = { (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F',
-		(byte)'G', (byte)'H', (byte)'I', (byte)'J', (byte)'K',
-		(byte)'L', (byte)'M', (byte)'N', (byte)'O', (byte)'P',
-		(byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U',
-		(byte)'V', (byte)'W', (byte)'X', (byte)'Y', (byte)'Z',
-		(byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e',
-		(byte)'f', (byte)'g', (byte)'h', (byte)'i', (byte)'j',
-		(byte)'k', (byte)'l', (byte)'m', (byte)'n', (byte)'o',
-		(byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t',
-		(byte)'u', (byte)'v', (byte)'w', (byte)'x', (byte)'y',
-		(byte)'z', (byte)'0', (byte)'1', (byte)'2', (byte)'3',
-		(byte)'4', (byte)'5', (byte)'6', (byte)'7', (byte)'8',
-		(byte)'9', (byte)'+', (byte)'/' };
-
-	/**
+  private final static byte[] ALPHABET =
+      {(byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F',
+          (byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K',
+          (byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P',
+          (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U',
+          (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z',
+          (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e',
+          (byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j',
+          (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o',
+          (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't',
+          (byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y',
+          (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3',
+          (byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8',
+          (byte) '9', (byte) '+', (byte) '/'};
+
+  /**
    * The 64 valid web safe Base64 values.
    */
-	private final static byte[] WEBSAFE_ALPHABET = { (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F',
-		(byte)'G', (byte)'H', (byte)'I', (byte)'J', (byte)'K',
-		(byte)'L', (byte)'M', (byte)'N', (byte)'O', (byte)'P',
-		(byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U',
-		(byte)'V', (byte)'W', (byte)'X', (byte)'Y', (byte)'Z',
-		(byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e',
-		(byte)'f', (byte)'g', (byte)'h', (byte)'i', (byte)'j',
-		(byte)'k', (byte)'l', (byte)'m', (byte)'n', (byte)'o',
-		(byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t',
-		(byte)'u', (byte)'v', (byte)'w', (byte)'x', (byte)'y',
-		(byte)'z', (byte)'0', (byte)'1', (byte)'2', (byte)'3',
-		(byte)'4', (byte)'5', (byte)'6', (byte)'7', (byte)'8',
-		(byte)'9', (byte)'-', (byte)'_' };
-
-	/**
+  private final static byte[] WEBSAFE_ALPHABET =
+      {(byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F',
+          (byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K',
+          (byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P',
+          (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U',
+          (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z',
+          (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e',
+          (byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j',
+          (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o',
+          (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't',
+          (byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y',
+          (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3',
+          (byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8',
+          (byte) '9', (byte) '-', (byte) '_'};
+
+  /**
    * Translates a Base64 value to either its 6-bit reconstruction value
    * or a negative number indicating some other meaning.
    **/
-	private final static byte[] DECODABET = {
-		-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal  0 -  8
-		-5, -5, // Whitespace: Tab and Linefeed
-		-9, -9, // Decimal 11 - 12
-		-5, // Whitespace: Carriage Return
-		-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26
-		-9, -9, -9, -9, -9, // Decimal 27 - 31
-		-5, // Whitespace: Space
-		-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42
-		62, // Plus sign at decimal 43
-		-9, -9, -9, // Decimal 44 - 46
-		63, // Slash at decimal 47
-		52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine
-		-9, -9, -9, // Decimal 58 - 60
-		-1, // Equals sign at decimal 61
-		-9, -9, -9, // Decimal 62 - 64
-		0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N'
-		14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z'
-		-9, -9, -9, -9, -9, -9, // Decimal 91 - 96
-		26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm'
-		39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z'
-		-9, -9, -9, -9, -9 // Decimal 123 - 127
-		/*  ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 128 - 139
+  private final static byte[] DECODABET = {-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal  0 -  8
+      -5, -5, // Whitespace: Tab and Linefeed
+      -9, -9, // Decimal 11 - 12
+      -5, // Whitespace: Carriage Return
+      -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26
+      -9, -9, -9, -9, -9, // Decimal 27 - 31
+      -5, // Whitespace: Space
+      -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42
+      62, // Plus sign at decimal 43
+      -9, -9, -9, // Decimal 44 - 46
+      63, // Slash at decimal 47
+      52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine
+      -9, -9, -9, // Decimal 58 - 60
+      -1, // Equals sign at decimal 61
+      -9, -9, -9, // Decimal 62 - 64
+      0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N'
+      14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z'
+      -9, -9, -9, -9, -9, -9, // Decimal 91 - 96
+      26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm'
+      39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z'
+      -9, -9, -9, -9, -9 // Decimal 123 - 127
+      /*  ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 128 - 139
         -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 140 - 152
         -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 153 - 165
         -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 166 - 178
@@ -123,33 +126,33 @@ public class Base64 {
         -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 218 - 230
         -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 231 - 243
         -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9         // Decimal 244 - 255 */
-	};
-
-	/** The web safe decodabet */
-	private final static byte[] WEBSAFE_DECODABET = {
-		-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal  0 -  8
-		-5, -5, // Whitespace: Tab and Linefeed
-		-9, -9, // Decimal 11 - 12
-		-5, // Whitespace: Carriage Return
-		-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26
-		-9, -9, -9, -9, -9, // Decimal 27 - 31
-		-5, // Whitespace: Space
-		-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 44
-		62, // Dash '-' sign at decimal 45
-		-9, -9, // Decimal 46-47
-		52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine
-		-9, -9, -9, // Decimal 58 - 60
-		-1, // Equals sign at decimal 61
-		-9, -9, -9, // Decimal 62 - 64
-		0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N'
-		14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z'
-		-9, -9, -9, -9, // Decimal 91-94
-		63, // Underscore '_' at decimal 95
-		-9, // Decimal 96
-		26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm'
-		39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z'
-		-9, -9, -9, -9, -9 // Decimal 123 - 127
-		/*  ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 128 - 139
+      };
+
+  /** The web safe decodabet */
+  private final static byte[] WEBSAFE_DECODABET =
+      {-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal  0 -  8
+          -5, -5, // Whitespace: Tab and Linefeed
+          -9, -9, // Decimal 11 - 12
+          -5, // Whitespace: Carriage Return
+          -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26
+          -9, -9, -9, -9, -9, // Decimal 27 - 31
+          -5, // Whitespace: Space
+          -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 44
+          62, // Dash '-' sign at decimal 45
+          -9, -9, // Decimal 46-47
+          52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine
+          -9, -9, -9, // Decimal 58 - 60
+          -1, // Equals sign at decimal 61
+          -9, -9, -9, // Decimal 62 - 64
+          0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N'
+          14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z'
+          -9, -9, -9, -9, // Decimal 91-94
+          63, // Underscore '_' at decimal 95
+          -9, // Decimal 96
+          26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm'
+          39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z'
+          -9, -9, -9, -9, -9 // Decimal 123 - 127
+      /*  ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 128 - 139
         -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 140 - 152
         -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 153 - 165
         -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 166 - 178
@@ -159,20 +162,20 @@ public class Base64 {
         -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 218 - 230
         -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 231 - 243
         -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9         // Decimal 244 - 255 */
-	};
+      };
 
-	// Indicates white space in encoding
-	private final static byte WHITE_SPACE_ENC = -5;
-	// Indicates equals sign in encoding
-	private final static byte EQUALS_SIGN_ENC = -1;
+  // Indicates white space in encoding
+  private final static byte WHITE_SPACE_ENC = -5;
+  // Indicates equals sign in encoding
+  private final static byte EQUALS_SIGN_ENC = -1;
 
-	/** Defeats instantiation. */
-	private Base64() {
-	}
+  /** Defeats instantiation. */
+  private Base64() {
+  }
 
-	/* ********  E N C O D I N G   M E T H O D S  ******** */
+  /* ********  E N C O D I N G   M E T H O D S  ******** */
 
-	/**
+  /**
    * Encodes up to three bytes of the array <var>source</var>
    * and writes the resulting four Base64 bytes to <var>destination</var>.
    * The source and destination arrays can be manipulated
@@ -194,47 +197,49 @@ public class Base64 {
    * @return the <var>destination</var> array
    * @since 1.3
    */
-	private static byte[] encode3to4(byte[] source, int srcOffset,
-			int numSigBytes, byte[] destination, int destOffset, byte[] alphabet) {
-		//           1         2         3
-		// 01234567890123456789012345678901 Bit position
-		// --------000000001111111122222222 Array position from threeBytes
-		// --------|    ||    ||    ||    | Six bit groups to index alphabet
-		//          >>18  >>12  >> 6  >> 0  Right shift necessary
-		//                0x3f  0x3f  0x3f  Additional AND
-
-		// Create buffer with zero-padding if there are only one or two
-		// significant bytes passed in the array.
-		// We have to shift left 24 in order to flush out the 1's that appear
-		// when Java treats a value as negative that is cast from a byte to an int.
-		int inBuff =
-				(numSigBytes > 0 ? ((source[srcOffset] << 24) >>> 8) : 0) | (numSigBytes > 1 ? ((source[srcOffset + 1] << 24) >>> 16) : 0) | (numSigBytes > 2 ? ((source[srcOffset + 2] << 24) >>> 24) : 0);
-
-		switch (numSigBytes) {
-			case 3:
-				destination[destOffset] = alphabet[(inBuff >>> 18)];
-				destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f];
-				destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f];
-				destination[destOffset + 3] = alphabet[(inBuff)&0x3f];
-				return destination;
-			case 2:
-				destination[destOffset] = alphabet[(inBuff >>> 18)];
-				destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f];
-				destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f];
-				destination[destOffset + 3] = EQUALS_SIGN;
-				return destination;
-			case 1:
-				destination[destOffset] = alphabet[(inBuff >>> 18)];
-				destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f];
-				destination[destOffset + 2] = EQUALS_SIGN;
-				destination[destOffset + 3] = EQUALS_SIGN;
-				return destination;
-			default:
-				return destination;
-		} // end switch
-	} // end encode3to4
-
-	/**
+  private static byte[] encode3to4(byte[] source, int srcOffset,
+      int numSigBytes, byte[] destination, int destOffset, byte[] alphabet) {
+    //           1         2         3
+    // 01234567890123456789012345678901 Bit position
+    // --------000000001111111122222222 Array position from threeBytes
+    // --------|    ||    ||    ||    | Six bit groups to index alphabet
+    //          >>18  >>12  >> 6  >> 0  Right shift necessary
+    //                0x3f  0x3f  0x3f  Additional AND
+
+    // Create buffer with zero-padding if there are only one or two
+    // significant bytes passed in the array.
+    // We have to shift left 24 in order to flush out the 1's that appear
+    // when Java treats a value as negative that is cast from a byte to an int.
+    int inBuff =
+        (numSigBytes > 0 ? ((source[srcOffset] << 24) >>> 8) : 0)
+            | (numSigBytes > 1 ? ((source[srcOffset + 1] << 24) >>> 16) : 0)
+            | (numSigBytes > 2 ? ((source[srcOffset + 2] << 24) >>> 24) : 0);
+
+    switch (numSigBytes) {
+      case 3:
+        destination[destOffset] = alphabet[(inBuff >>> 18)];
+        destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f];
+        destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f];
+        destination[destOffset + 3] = alphabet[(inBuff) & 0x3f];
+        return destination;
+      case 2:
+        destination[destOffset] = alphabet[(inBuff >>> 18)];
+        destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f];
+        destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f];
+        destination[destOffset + 3] = EQUALS_SIGN;
+        return destination;
+      case 1:
+        destination[destOffset] = alphabet[(inBuff >>> 18)];
+        destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f];
+        destination[destOffset + 2] = EQUALS_SIGN;
+        destination[destOffset + 3] = EQUALS_SIGN;
+        return destination;
+      default:
+        return destination;
+    } // end switch
+  } // end encode3to4
+
+  /**
    * Encodes a byte array into Base64 notation.
    * Equivalent to calling
    * {@code encodeBytes(source, 0, source.length)}
@@ -242,22 +247,22 @@ public class Base64 {
    * @param source The data to convert
    * @since 1.4
    */
-	public static String encode(byte[] source) {
-		return encode(source, 0, source.length, ALPHABET, true);
-	}
+  public static String encode(byte[] source) {
+    return encode(source, 0, source.length, ALPHABET, true);
+  }
 
-	/**
+  /**
    * Encodes a byte array into web safe Base64 notation.
    *
    * @param source The data to convert
    * @param doPadding is {@code true} to pad result with '=' chars
    *        if it does not fall on 3 byte boundaries
    */
-	public static String encodeWebSafe(byte[] source, boolean doPadding) {
-		return encode(source, 0, source.length, WEBSAFE_ALPHABET, doPadding);
-	}
+  public static String encodeWebSafe(byte[] source, boolean doPadding) {
+    return encode(source, 0, source.length, WEBSAFE_ALPHABET, doPadding);
+  }
 
-	/**
+  /**
    * Encodes a byte array into Base64 notation.
    *
    * @param source The data to convert
@@ -268,24 +273,24 @@ public class Base64 {
    *        if it does not fall on 3 byte boundaries
    * @since 1.4
    */
-	public static String encode(byte[] source, int off, int len, byte[] alphabet,
-			boolean doPadding) {
-		byte[] outBuff = encode(source, off, len, alphabet, Integer.MAX_VALUE);
-		int outLen = outBuff.length;
-
-		// If doPadding is false, set length to truncate '='
-		// padding characters
-		while (doPadding == false && outLen > 0) {
-			if (outBuff[outLen - 1] != '=') {
-				break;
-			}
-			outLen -= 1;
-		}
-
-		return new String(outBuff, 0, outLen);
-	}
-
-	/**
+  public static String encode(byte[] source, int off, int len, byte[] alphabet,
+      boolean doPadding) {
+    byte[] outBuff = encode(source, off, len, alphabet, Integer.MAX_VALUE);
+    int outLen = outBuff.length;
+
+    // If doPadding is false, set length to truncate '='
+    // padding characters
+    while (doPadding == false && outLen > 0) {
+      if (outBuff[outLen - 1] != '=') {
+        break;
+      }
+      outLen -= 1;
+    }
+
+    return new String(outBuff, 0, outLen);
+  }
+
+  /**
    * Encodes a byte array into Base64 notation.
    *
    * @param source The data to convert
@@ -295,57 +300,64 @@ public class Base64 {
    * @param maxLineLength maximum length of one line.
    * @return the BASE64-encoded byte array
    */
-	public static byte[] encode(byte[] source, int off, int len, byte[] alphabet,
-			int maxLineLength) {
-		int lenDiv3 = (len + 2) / 3; // ceil(len / 3)
-		int len43 = lenDiv3 * 4;
-		byte[] outBuff = new byte[len43 // Main 4:3
-								  + (len43 / maxLineLength)]; // New lines
-
-		int d = 0;
-		int e = 0;
-		int len2 = len - 2;
-		int lineLength = 0;
-		for (; d < len2; d += 3, e += 4) {
-
-			// The following block of code is the same as
-			// encode3to4( source, d + off, 3, outBuff, e, alphabet );
-			// but inlined for faster encoding (~20% improvement)
-			int inBuff =
-					((source[d + off] << 24) >>> 8) | ((source[d + 1 + off] << 24) >>> 16) | ((source[d + 2 + off] << 24) >>> 24);
-			outBuff[e] = alphabet[(inBuff >>> 18)];
-			outBuff[e + 1] = alphabet[(inBuff >>> 12) & 0x3f];
-			outBuff[e + 2] = alphabet[(inBuff >>> 6) & 0x3f];
-			outBuff[e + 3] = alphabet[(inBuff)&0x3f];
-
-			lineLength += 4;
-			if (lineLength == maxLineLength) {
-				outBuff[e + 4] = NEW_LINE;
-				e++;
-				lineLength = 0;
-			} // end if: end of line
-		} // end for: each piece of array
-
-		if (d < len) {
-			encode3to4(source, d + off, len - d, outBuff, e, alphabet);
-
-			lineLength += 4;
-			if (lineLength == maxLineLength) {
-				// Add a last newline
-				outBuff[e + 4] = NEW_LINE;
-				e++;
-			}
-			e += 4;
-		}
-
-		if (BuildConfig.DEBUG && e != outBuff.length)
-			throw new RuntimeException();
-		return outBuff;
-	}
-
-	/* ********  D E C O D I N G   M E T H O D S  ******** */
-
-	/**
+  public static byte[] encode(byte[] source, int off, int len, byte[] alphabet,
+      int maxLineLength) {
+    int lenDiv3 = (len + 2) / 3; // ceil(len / 3)
+    int len43 = lenDiv3 * 4;
+    byte[] outBuff = new byte[len43 // Main 4:3
+        + (len43 / maxLineLength)]; // New lines
+
+    int d = 0;
+    int e = 0;
+    int len2 = len - 2;
+    int lineLength = 0;
+    for (; d < len2; d += 3, e += 4) {
+
+      // The following block of code is the same as
+      // encode3to4( source, d + off, 3, outBuff, e, alphabet );
+      // but inlined for faster encoding (~20% improvement)
+      int inBuff =
+          ((source[d + off] << 24) >>> 8)
+              | ((source[d + 1 + off] << 24) >>> 16)
+              | ((source[d + 2 + off] << 24) >>> 24);
+      outBuff[e] = alphabet[(inBuff >>> 18)];
+      outBuff[e + 1] = alphabet[(inBuff >>> 12) & 0x3f];
+      outBuff[e + 2] = alphabet[(inBuff >>> 6) & 0x3f];
+      outBuff[e + 3] = alphabet[(inBuff) & 0x3f];
+
+      lineLength += 4;
+      if (lineLength == maxLineLength) {
+        outBuff[e + 4] = NEW_LINE;
+        e++;
+        lineLength = 0;
+      } // end if: end of line
+    } // end for: each piece of array
+
+    if (d < len) {
+      encode3to4(source, d + off, len - d, outBuff, e, alphabet);
+
+      lineLength += 4;
+      if (lineLength == maxLineLength) {
+        // Add a last newline
+        outBuff[e + 4] = NEW_LINE;
+        e++;
+      }
+      e += 4;
+    }
+
+    // -- GODOT start --
+    //assert (e == outBuff.length);
+    if (BuildConfig.DEBUG && e != outBuff.length)
+      throw new RuntimeException();
+    // -- GODOT end --
+    return outBuff;
+  }
+
+
+  /* ********  D E C O D I N G   M E T H O D S  ******** */
+
+
+  /**
    * Decodes four bytes from array <var>source</var>
    * and writes the resulting bytes (up to three of them)
    * to <var>destination</var>.
@@ -368,60 +380,67 @@ public class Base64 {
    * @return the number of decoded bytes converted
    * @since 1.3
    */
-	private static int decode4to3(byte[] source, int srcOffset,
-			byte[] destination, int destOffset, byte[] decodabet) {
-		// Example: Dk==
-		if (source[srcOffset + 2] == EQUALS_SIGN) {
-			int outBuff =
-					((decodabet[source[srcOffset]] << 24) >>> 6) | ((decodabet[source[srcOffset + 1]] << 24) >>> 12);
-
-			destination[destOffset] = (byte)(outBuff >>> 16);
-			return 1;
-		} else if (source[srcOffset + 3] == EQUALS_SIGN) {
-			// Example: DkL=
-			int outBuff =
-					((decodabet[source[srcOffset]] << 24) >>> 6) | ((decodabet[source[srcOffset + 1]] << 24) >>> 12) | ((decodabet[source[srcOffset + 2]] << 24) >>> 18);
-
-			destination[destOffset] = (byte)(outBuff >>> 16);
-			destination[destOffset + 1] = (byte)(outBuff >>> 8);
-			return 2;
-		} else {
-			// Example: DkLE
-			int outBuff =
-					((decodabet[source[srcOffset]] << 24) >>> 6) | ((decodabet[source[srcOffset + 1]] << 24) >>> 12) | ((decodabet[source[srcOffset + 2]] << 24) >>> 18) | ((decodabet[source[srcOffset + 3]] << 24) >>> 24);
-
-			destination[destOffset] = (byte)(outBuff >> 16);
-			destination[destOffset + 1] = (byte)(outBuff >> 8);
-			destination[destOffset + 2] = (byte)(outBuff);
-			return 3;
-		}
-	} // end decodeToBytes
-
-	/**
+  private static int decode4to3(byte[] source, int srcOffset,
+      byte[] destination, int destOffset, byte[] decodabet) {
+    // Example: Dk==
+    if (source[srcOffset + 2] == EQUALS_SIGN) {
+      int outBuff =
+          ((decodabet[source[srcOffset]] << 24) >>> 6)
+              | ((decodabet[source[srcOffset + 1]] << 24) >>> 12);
+
+      destination[destOffset] = (byte) (outBuff >>> 16);
+      return 1;
+    } else if (source[srcOffset + 3] == EQUALS_SIGN) {
+      // Example: DkL=
+      int outBuff =
+          ((decodabet[source[srcOffset]] << 24) >>> 6)
+              | ((decodabet[source[srcOffset + 1]] << 24) >>> 12)
+              | ((decodabet[source[srcOffset + 2]] << 24) >>> 18);
+
+      destination[destOffset] = (byte) (outBuff >>> 16);
+      destination[destOffset + 1] = (byte) (outBuff >>> 8);
+      return 2;
+    } else {
+      // Example: DkLE
+      int outBuff =
+          ((decodabet[source[srcOffset]] << 24) >>> 6)
+              | ((decodabet[source[srcOffset + 1]] << 24) >>> 12)
+              | ((decodabet[source[srcOffset + 2]] << 24) >>> 18)
+              | ((decodabet[source[srcOffset + 3]] << 24) >>> 24);
+
+      destination[destOffset] = (byte) (outBuff >> 16);
+      destination[destOffset + 1] = (byte) (outBuff >> 8);
+      destination[destOffset + 2] = (byte) (outBuff);
+      return 3;
+    }
+  } // end decodeToBytes
+
+
+  /**
    * Decodes data from Base64 notation.
    *
    * @param s the string to decode (decoded in default encoding)
    * @return the decoded data
    * @since 1.4
    */
-	public static byte[] decode(String s) throws Base64DecoderException {
-		byte[] bytes = s.getBytes();
-		return decode(bytes, 0, bytes.length);
-	}
+  public static byte[] decode(String s) throws Base64DecoderException {
+    byte[] bytes = s.getBytes();
+    return decode(bytes, 0, bytes.length);
+  }
 
-	/**
+  /**
    * Decodes data from web safe Base64 notation.
    * Web safe encoding uses '-' instead of '+', '_' instead of '/'
    *
    * @param s the string to decode (decoded in default encoding)
    * @return the decoded data
    */
-	public static byte[] decodeWebSafe(String s) throws Base64DecoderException {
-		byte[] bytes = s.getBytes();
-		return decodeWebSafe(bytes, 0, bytes.length);
-	}
+  public static byte[] decodeWebSafe(String s) throws Base64DecoderException {
+    byte[] bytes = s.getBytes();
+    return decodeWebSafe(bytes, 0, bytes.length);
+  }
 
-	/**
+  /**
    * Decodes Base64 content in byte array format and returns
    * the decoded byte array.
    *
@@ -430,11 +449,11 @@ public class Base64 {
    * @since 1.3
    * @throws Base64DecoderException
    */
-	public static byte[] decode(byte[] source) throws Base64DecoderException {
-		return decode(source, 0, source.length);
-	}
+  public static byte[] decode(byte[] source) throws Base64DecoderException {
+    return decode(source, 0, source.length);
+  }
 
-	/**
+  /**
    * Decodes web safe Base64 content in byte array format and returns
    * the decoded data.
    * Web safe encoding uses '-' instead of '+', '_' instead of '/'
@@ -442,12 +461,12 @@ public class Base64 {
    * @param source the string to decode (decoded in default encoding)
    * @return the decoded data
    */
-	public static byte[] decodeWebSafe(byte[] source)
-			throws Base64DecoderException {
-		return decodeWebSafe(source, 0, source.length);
-	}
+  public static byte[] decodeWebSafe(byte[] source)
+      throws Base64DecoderException {
+    return decodeWebSafe(source, 0, source.length);
+  }
 
-	/**
+  /**
    * Decodes Base64 content in byte array format and returns
    * the decoded byte array.
    *
@@ -458,12 +477,12 @@ public class Base64 {
    * @since 1.3
    * @throws Base64DecoderException
    */
-	public static byte[] decode(byte[] source, int off, int len)
-			throws Base64DecoderException {
-		return decode(source, off, len, DECODABET);
-	}
+  public static byte[] decode(byte[] source, int off, int len)
+      throws Base64DecoderException {
+    return decode(source, off, len, DECODABET);
+  }
 
-	/**
+  /**
    * Decodes web safe Base64 content in byte array format and returns
    * the decoded byte array.
    * Web safe encoding uses '-' instead of '+', '_' instead of '/'
@@ -473,12 +492,12 @@ public class Base64 {
    * @param len    The length of characters to decode
    * @return decoded data
    */
-	public static byte[] decodeWebSafe(byte[] source, int off, int len)
-			throws Base64DecoderException {
-		return decode(source, off, len, WEBSAFE_DECODABET);
-	}
+  public static byte[] decodeWebSafe(byte[] source, int off, int len)
+      throws Base64DecoderException {
+    return decode(source, off, len, WEBSAFE_DECODABET);
+  }
 
-	/**
+  /**
    * Decodes Base64 content using the supplied decodabet and returns
    * the decoded byte array.
    *
@@ -488,69 +507,72 @@ public class Base64 {
    * @param decodabet the decodabet for decoding Base64 content
    * @return decoded data
    */
-	public static byte[] decode(byte[] source, int off, int len, byte[] decodabet)
-			throws Base64DecoderException {
-		int len34 = len * 3 / 4;
-		byte[] outBuff = new byte[2 + len34]; // Upper limit on size of output
-		int outBuffPosn = 0;
-
-		byte[] b4 = new byte[4];
-		int b4Posn = 0;
-		int i = 0;
-		byte sbiCrop = 0;
-		byte sbiDecode = 0;
-		for (i = 0; i < len; i++) {
-			sbiCrop = (byte)(source[i + off] & 0x7f); // Only the low seven bits
-			sbiDecode = decodabet[sbiCrop];
-
-			if (sbiDecode >= WHITE_SPACE_ENC) { // White space Equals sign or better
-				if (sbiDecode >= EQUALS_SIGN_ENC) {
-					// An equals sign (for padding) must not occur at position 0 or 1
-					// and must be the last byte[s] in the encoded value
-					if (sbiCrop == EQUALS_SIGN) {
-						int bytesLeft = len - i;
-						byte lastByte = (byte)(source[len - 1 + off] & 0x7f);
-						if (b4Posn == 0 || b4Posn == 1) {
-							throw new Base64DecoderException(
-									"invalid padding byte '=' at byte offset " + i);
-						} else if ((b4Posn == 3 && bytesLeft > 2) || (b4Posn == 4 && bytesLeft > 1)) {
-							throw new Base64DecoderException(
-									"padding byte '=' falsely signals end of encoded value "
-									+ "at offset " + i);
-						} else if (lastByte != EQUALS_SIGN && lastByte != NEW_LINE) {
-							throw new Base64DecoderException(
-									"encoded value has invalid trailing byte");
-						}
-						break;
-					}
-
-					b4[b4Posn++] = sbiCrop;
-					if (b4Posn == 4) {
-						outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet);
-						b4Posn = 0;
-					}
-				}
-			} else {
-				throw new Base64DecoderException("Bad Base64 input character at " + i + ": " + source[i + off] + "(decimal)");
-			}
-		}
-
-		// Because web safe encoding allows non padding base64 encodes, we
-		// need to pad the rest of the b4 buffer with equal signs when
-		// b4Posn != 0.  There can be at most 2 equal signs at the end of
-		// four characters, so the b4 buffer must have two or three
-		// characters.  This also catches the case where the input is
-		// padded with EQUALS_SIGN
-		if (b4Posn != 0) {
-			if (b4Posn == 1) {
-				throw new Base64DecoderException("single trailing character at offset " + (len - 1));
-			}
-			b4[b4Posn++] = EQUALS_SIGN;
-			outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet);
-		}
-
-		byte[] out = new byte[outBuffPosn];
-		System.arraycopy(outBuff, 0, out, 0, outBuffPosn);
-		return out;
-	}
+  public static byte[] decode(byte[] source, int off, int len, byte[] decodabet)
+      throws Base64DecoderException {
+    int len34 = len * 3 / 4;
+    byte[] outBuff = new byte[2 + len34]; // Upper limit on size of output
+    int outBuffPosn = 0;
+
+    byte[] b4 = new byte[4];
+    int b4Posn = 0;
+    int i = 0;
+    byte sbiCrop = 0;
+    byte sbiDecode = 0;
+    for (i = 0; i < len; i++) {
+      sbiCrop = (byte) (source[i + off] & 0x7f); // Only the low seven bits
+      sbiDecode = decodabet[sbiCrop];
+
+      if (sbiDecode >= WHITE_SPACE_ENC) { // White space Equals sign or better
+        if (sbiDecode >= EQUALS_SIGN_ENC) {
+          // An equals sign (for padding) must not occur at position 0 or 1
+          // and must be the last byte[s] in the encoded value
+          if (sbiCrop == EQUALS_SIGN) {
+            int bytesLeft = len - i;
+            byte lastByte = (byte) (source[len - 1 + off] & 0x7f);
+            if (b4Posn == 0 || b4Posn == 1) {
+              throw new Base64DecoderException(
+                  "invalid padding byte '=' at byte offset " + i);
+            } else if ((b4Posn == 3 && bytesLeft > 2)
+                || (b4Posn == 4 && bytesLeft > 1)) {
+              throw new Base64DecoderException(
+                  "padding byte '=' falsely signals end of encoded value "
+                      + "at offset " + i);
+            } else if (lastByte != EQUALS_SIGN && lastByte != NEW_LINE) {
+              throw new Base64DecoderException(
+                  "encoded value has invalid trailing byte");
+            }
+            break;
+          }
+
+          b4[b4Posn++] = sbiCrop;
+          if (b4Posn == 4) {
+            outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet);
+            b4Posn = 0;
+          }
+        }
+      } else {
+        throw new Base64DecoderException("Bad Base64 input character at " + i
+            + ": " + source[i + off] + "(decimal)");
+      }
+    }
+
+    // Because web safe encoding allows non padding base64 encodes, we
+    // need to pad the rest of the b4 buffer with equal signs when
+    // b4Posn != 0.  There can be at most 2 equal signs at the end of
+    // four characters, so the b4 buffer must have two or three
+    // characters.  This also catches the case where the input is
+    // padded with EQUALS_SIGN
+    if (b4Posn != 0) {
+      if (b4Posn == 1) {
+        throw new Base64DecoderException("single trailing character at offset "
+            + (len - 1));
+      }
+      b4[b4Posn++] = EQUALS_SIGN;
+      outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet);
+    }
+
+    byte[] out = new byte[outBuffPosn];
+    System.arraycopy(outBuff, 0, out, 0, outBuffPosn);
+    return out;
+  }
 }

+ 7 - 7
platform/android/java/src/com/google/android/vending/licensing/util/Base64DecoderException.java

@@ -20,13 +20,13 @@ package com.google.android.vending.licensing.util;
  * @author nelson
  */
 public class Base64DecoderException extends Exception {
-	public Base64DecoderException() {
-		super();
-	}
+  public Base64DecoderException() {
+    super();
+  }
 
-	public Base64DecoderException(String s) {
-		super(s);
-	}
+  public Base64DecoderException(String s) {
+    super(s);
+  }
 
-	private static final long serialVersionUID = 1L;
+  private static final long serialVersionUID = 1L;
 }

+ 25 - 25
platform/android/java/src/com/google/android/vending/licensing/util/URIQueryDecoder.java

@@ -25,36 +25,36 @@ import java.util.Map;
 import java.util.Scanner;
 
 public class URIQueryDecoder {
-	private static final String TAG = "URIQueryDecoder";
+    private static final String TAG = "URIQueryDecoder";
 
-	/**
+    /**
      * Decodes the query portion of the passed-in URI.
      *
      * @param encodedURI the URI containing the query to decode
      * @param results a map containing all query parameters. Query parameters that do not have a
      *            value will map to a null string
      */
-	static public void DecodeQuery(URI encodedURI, Map<String, String> results) {
-		Scanner scanner = new Scanner(encodedURI.getRawQuery());
-		scanner.useDelimiter("&");
-		try {
-			while (scanner.hasNext()) {
-				String param = scanner.next();
-				String[] valuePair = param.split("=");
-				String name, value;
-				if (valuePair.length == 1) {
-					value = null;
-				} else if (valuePair.length == 2) {
-					value = URLDecoder.decode(valuePair[1], "UTF-8");
-				} else {
-					throw new IllegalArgumentException("query parameter invalid");
-				}
-				name = URLDecoder.decode(valuePair[0], "UTF-8");
-				results.put(name, value);
-			}
-		} catch (UnsupportedEncodingException e) {
-			// This should never happen.
-			Log.e(TAG, "UTF-8 Not Recognized as a charset.  Device configuration Error.");
-		}
-	}
+    static public void DecodeQuery(URI encodedURI, Map<String, String> results) {
+        Scanner scanner = new Scanner(encodedURI.getRawQuery());
+        scanner.useDelimiter("&");
+        try {
+            while (scanner.hasNext()) {
+                String param = scanner.next();
+                String[] valuePair = param.split("=");
+                String name, value;
+                if (valuePair.length == 1) {
+                    value = null;
+                } else if (valuePair.length == 2) {
+                    value = URLDecoder.decode(valuePair[1], "UTF-8");
+                } else {
+                    throw new IllegalArgumentException("query parameter invalid");
+                }
+                name = URLDecoder.decode(valuePair[0], "UTF-8");
+                results.put(name, value);
+            }
+        } catch (UnsupportedEncodingException e) {
+            // This should never happen.
+            Log.e(TAG, "UTF-8 Not Recognized as a charset.  Device configuration Error.");
+        }
+    }
 }

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini