diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..6863a18e --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,2 @@ +/.github/workflows/ @libgdx/libgdx-signing +/.github/CODEOWNERS @libgdx/libgdx-signing diff --git a/.github/workflows/compile.yml b/.github/workflows/compile.yml index e827d313..f7041ab8 100644 --- a/.github/workflows/compile.yml +++ b/.github/workflows/compile.yml @@ -14,13 +14,13 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Set up JDK 1.8 + - name: Set up JDK 11 uses: actions/setup-java@v1 with: - java-version: 1.8 + java-version: 11 - name: Grant execute permission for gradlew run: chmod +x gradlew - name: Setup Android build environment uses: android-actions/setup-android@v2.0.2 - name: Local install - run: ./gradlew clean uploadArchives -PLOCAL=true + run: ./gradlew clean publishToMavenLocal diff --git a/.github/workflows/publish_release.yml b/.github/workflows/publish_release.yml index ad8ec551..6904692d 100644 --- a/.github/workflows/publish_release.yml +++ b/.github/workflows/publish_release.yml @@ -14,19 +14,19 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Set up JDK 1.8 + - name: Set up JDK 11 uses: actions/setup-java@v1 with: - java-version: 1.8 + java-version: 11 - name: Grant execute permission for gradlew run: chmod +x gradlew - name: Setup Android build environment uses: android-actions/setup-android@v2.0.2 - name: Compile - run: ./gradlew uploadArchives -PLOCAL=true + run: ./gradlew publishToMavenLocal - name: Import GPG key id: import_gpg - uses: crazy-max/ghaction-import-gpg@v3 + uses: crazy-max/ghaction-import-gpg@1c6a9e9d3594f2d743f1b1dd7669ab0dfdffa922 with: gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} passphrase: ${{ secrets.GPG_PASSPHRASE }} @@ -35,4 +35,4 @@ jobs: NEXUS_USERNAME: ${{ secrets.NEXUS_USERNAME }} NEXUS_PASSWORD: ${{ secrets.NEXUS_PASSWORD }} run: - ./gradlew clean uploadArchives -PRELEASE=true -Psigning.gnupg.keyId=${{ secrets.GPG_KEYID }} -Psigning.gnupg.passphrase=${{ secrets.GPG_PASSPHRASE }} -Psigning.gnupg.keyName=${{ secrets.GPG_KEYID }} + ./gradlew clean publish -PRELEASE=true -Psigning.gnupg.keyId=${{ secrets.GPG_KEYID }} -Psigning.gnupg.passphrase=${{ secrets.GPG_PASSPHRASE }} -Psigning.gnupg.keyName=${{ secrets.GPG_KEYID }} diff --git a/.github/workflows/publish_snapshot.yml b/.github/workflows/publish_snapshot.yml index a2878db2..18e54647 100644 --- a/.github/workflows/publish_snapshot.yml +++ b/.github/workflows/publish_snapshot.yml @@ -14,18 +14,18 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Set up JDK 1.8 + - name: Set up JDK 11 uses: actions/setup-java@v1 with: - java-version: 1.8 + java-version: 11 - name: Grant execute permission for gradlew run: chmod +x gradlew - name: Setup Android build environment uses: android-actions/setup-android@v2.0.2 - name: Compile - run: ./gradlew uploadArchives -PLOCAL=true + run: ./gradlew publishToMavenLocal - name: Publish snapshot env: NEXUS_USERNAME: ${{ secrets.NEXUS_USERNAME }} NEXUS_PASSWORD: ${{ secrets.NEXUS_PASSWORD }} - run: ./gradlew uploadArchives + run: ./gradlew publish diff --git a/README.md b/README.md index 48204f64..9fbae894 100644 --- a/README.md +++ b/README.md @@ -144,7 +144,7 @@ Something not working quite as expected? Do you need a feature that has not been To build from source, clone or download this repository, then open it in Android Studio. Perform the following command to compile and upload the library in your local repository: - ./gradlew assemble uploadArchives -PLOCAL + ./gradlew publishToMavenLocal See build.gradle file for current version to use in your dependencies. diff --git a/build.gradle b/build.gradle index 534dd598..f811f594 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ -task wrapper(type: Wrapper) { - gradleVersion = '4.6' +tasks.named('wrapper') { + gradleVersion = '7.5.1' distributionUrl = "https://services.gradle.org/distributions/gradle-${gradleVersion}-all.zip" } @@ -10,9 +10,9 @@ class Developer { } ext { - androidBuildToolsVersion = '28.0.3' - androidCompileSdkVersion = 27 - androidTargetSdkVersion = 27 + androidBuildToolsVersion = '33.0.0' + androidCompileSdkVersion = 33 + androidTargetSdkVersion = 33 assertJVersion = '1.7.1' gdxVersion = '1.9.8' robolectricVersion = '4.3_r2-robolectric-0' @@ -83,7 +83,7 @@ buildscript { maven { url "https://oss.sonatype.org/content/repositories/snapshots/" } } dependencies { - classpath "com.android.tools.build:gradle:3.2.0" + classpath "com.android.tools.build:gradle:7.2.2" classpath "com.mobidevelop.robovm:robovm-gradle-plugin:${roboVMVersion}" classpath "de.mobilej.unmock:UnMockPlugin:${unmockVersion}" } diff --git a/gdx-pay-android-amazon/AndroidManifest.xml b/gdx-pay-android-amazon/AndroidManifest.xml index c827c7c3..c5098e95 100644 --- a/gdx-pay-android-amazon/AndroidManifest.xml +++ b/gdx-pay-android-amazon/AndroidManifest.xml @@ -6,8 +6,8 @@ + android:name="com.amazon.device.iap.ResponseReceiver" + tools:ignore="ExportedReceiver,InvalidPermission" android:exported="false"> Account details -> Licsensed) so they are not being charged +* Add testers to your closed alpha build so they see the new IAPs and also add these testers as "licensed testers" (Settings -> Account details -> Licensed) so they are not being charged * Testers must join the alpha test channel and can test (in my experiences they can also test with a debug build) diff --git a/gdx-pay-android-googlebilling/build.gradle b/gdx-pay-android-googlebilling/build.gradle index 88307b81..5e234f7e 100644 --- a/gdx-pay-android-googlebilling/build.gradle +++ b/gdx-pay-android-googlebilling/build.gradle @@ -3,8 +3,6 @@ apply plugin: 'com.android.library' apply from : '../publish_android.gradle' android { - defaultPublishConfig "release" - compileSdkVersion androidCompileSdkVersion buildToolsVersion androidBuildToolsVersion @@ -34,12 +32,12 @@ android { configurations { compileAndIncludeClassesInLibraryJar - compile.extendsFrom compileAndIncludeClassesInLibraryJar + api.extendsFrom compileAndIncludeClassesInLibraryJar } dependencies { - compile project(':gdx-pay-client') - compile 'com.android.billingclient:billing:3.0.1' + api project(':gdx-pay-client') + api 'com.android.billingclient:billing:5.0.0' - testCompile libraries.junit + testImplementation libraries.junit } diff --git a/gdx-pay-android-googlebilling/src/com/badlogic/gdx/pay/android/googlebilling/PurchaseManagerGoogleBilling.java b/gdx-pay-android-googlebilling/src/com/badlogic/gdx/pay/android/googlebilling/PurchaseManagerGoogleBilling.java index cd18bcab..3739e4f1 100644 --- a/gdx-pay-android-googlebilling/src/com/badlogic/gdx/pay/android/googlebilling/PurchaseManagerGoogleBilling.java +++ b/gdx-pay-android-googlebilling/src/com/badlogic/gdx/pay/android/googlebilling/PurchaseManagerGoogleBilling.java @@ -2,6 +2,7 @@ import android.app.Activity; import com.android.billingclient.api.*; +import com.android.billingclient.api.BillingClient.ProductType; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.pay.*; @@ -20,9 +21,10 @@ public class PurchaseManagerGoogleBilling implements PurchaseManager, PurchasesUpdatedListener { private static final String TAG = "GdxPay/GoogleBilling"; + private final Map informationMap = new ConcurrentHashMap<>(); private final Activity activity; - private final Map skuDetailsMap = new HashMap<>(); + private final Map productDetailsMap = new HashMap<>(); private boolean serviceConnected; private boolean installationComplete; private BillingClient mBillingClient; @@ -48,6 +50,8 @@ public String storeName() { @Override public void install(PurchaseObserver observer, PurchaseManagerConfig config, boolean autoFetchInformation) { + + Gdx.app.debug(TAG, "Called install()"); this.observer = observer; this.config = config; @@ -92,91 +96,165 @@ public void onBillingServiceDisconnected() { }); } - private String getGlobalSkuTypeFromConfig() { - String skuType = null; - for (int z = 0; z < config.getOfferCount(); z++) { - String offerSkuType = mapOfferType(config.getOffer(z).getType()); - if (skuType != null && !skuType.equals(offerSkuType)) { - throw new IllegalStateException("Cannot support OfferType Subscription and other types in the same app"); - } - skuType = offerSkuType; - } - - return skuType; - } - private void fetchOfferDetails() { - skuDetailsMap.clear(); + Gdx.app.debug(TAG,"Called fetchOfferDetails()"); + productDetailsMap.clear(); int offerSize = config.getOfferCount(); - List skuList = new ArrayList<>(offerSize); + + List productList = new ArrayList<>(); + for (int z = 0; z < offerSize; z++) { - skuList.add(config.getOffer(z).getIdentifierForStore(storeName())); + Offer offer = config.getOffer(z); + productList.add(QueryProductDetailsParams.Product.newBuilder() + .setProductId(offer.getIdentifierForStore(storeName())) + .setProductType(mapOfferType(offer.getType())) + .build()); } - if (skuList.isEmpty()) { - Gdx.app.log(TAG, "No skus configured"); + if (productList.isEmpty()) { + Gdx.app.debug(TAG, "No products configured"); setInstalledAndNotifyObserver(); return; } - mBillingClient.querySkuDetailsAsync( - SkuDetailsParams.newBuilder() - .setSkusList(skuList) - .setType(getGlobalSkuTypeFromConfig()) - .build(), - new SkuDetailsResponseListener() { - @Override - public void onSkuDetailsResponse(@Nonnull BillingResult result, List skuDetailsList) { - int responseCode = result.getResponseCode(); + QueryProductDetailsParams params = QueryProductDetailsParams.newBuilder() + .setProductList(productList) + .build(); + + Gdx.app.debug(TAG, "QueryProductDetailsParams: " + params); + mBillingClient.queryProductDetailsAsync( + params, + new ProductDetailsResponseListener() { + public void onProductDetailsResponse(@Nonnull BillingResult billingResult, @Nonnull List productDetailsList) { + int responseCode = billingResult.getResponseCode(); // it might happen that this was already disposed until the response comes back if (observer == null || Gdx.app == null) return; if (responseCode != BillingClient.BillingResponseCode.OK) { - Gdx.app.error(TAG, "onSkuDetailsResponse failed, error code is " + responseCode); - if (!installationComplete) - observer.handleInstallError(new FetchItemInformationException( - String.valueOf(responseCode))); + Gdx.app.error(TAG, "onProductDetailsResponse failed, error code is " + responseCode); + if (!installationComplete) { + observer.handleInstallError(new FetchItemInformationException(String.valueOf(responseCode))); + } } else { - if (skuDetailsList != null) { - - for (SkuDetails skuDetails : skuDetailsList) { - informationMap.put(skuDetails.getSku(), convertSkuDetailsToInformation - (skuDetails)); - skuDetailsMap.put(skuDetails.getSku(), skuDetails); - } - } else { - Gdx.app.log(TAG, "skuDetailsList is null"); + Gdx.app.debug(TAG,"Retrieved product count: " + productDetailsList.size()); + for (ProductDetails productDetails : productDetailsList) { + informationMap.put(productDetails.getProductId(), convertProductDetailsToInformation + (productDetails)); + productDetailsMap.put(productDetails.getProductId(), productDetails); } + setInstalledAndNotifyObserver(); } } - }); + } + ); } private String mapOfferType(OfferType type) { switch (type) { case CONSUMABLE: case ENTITLEMENT: - return BillingClient.SkuType.INAPP; + return ProductType.INAPP; case SUBSCRIPTION: - return BillingClient.SkuType.SUBS; + return ProductType.SUBS; } throw new IllegalStateException("Unsupported OfferType: " + type); } - private Information convertSkuDetailsToInformation(SkuDetails skuDetails) { - String priceString = skuDetails.getPrice(); - return Information.newBuilder() - .localName(skuDetails.getTitle()) - .freeTrialPeriod(convertToFreeTrialPeriod(skuDetails.getFreeTrialPeriod())) - .localDescription(skuDetails.getDescription()) + private Information convertProductDetailsToInformation(ProductDetails productDetails) { + Gdx.app.debug(TAG, "Converting productDetails: \n" + productDetails); + + Information.Builder builder = Information.newBuilder() + .localName(productDetails.getTitle()) + .localDescription(productDetails.getDescription()); + + if (ProductType.SUBS.equals(productDetails.getProductType())) { + convertSubscriptionProductToInformation(builder, productDetails.getSubscriptionOfferDetails()); + } else { + convertOneTimeProductToInformation(builder, productDetails.getOneTimePurchaseOfferDetails()); + } + return builder.build(); + } + + private void convertSubscriptionProductToInformation(Information.Builder builder, List subscriptionOfferDetails) { + if (subscriptionOfferDetails.isEmpty()) { + Gdx.app.error(TAG, "Empty SubscriptionOfferDetails"); + return; + } + ProductDetails.SubscriptionOfferDetails details = getActiveSubscriptionOfferDetails(subscriptionOfferDetails); + if (details.getPricingPhases().getPricingPhaseList().isEmpty()) { + Gdx.app.error(TAG, "getPricingPhases() or empty "); + return; + } + + + ProductDetails.PricingPhase paidForPricingPhase = getPaidRecurringPricingPhase(details); + if (paidForPricingPhase == null) { + Gdx.app.error(TAG, "no paidRecurringPricingPhase found "); + return; + + } + + builder.localPricing(paidForPricingPhase.getFormattedPrice()) + .priceCurrencyCode(paidForPricingPhase.getPriceCurrencyCode()) + .priceInCents((int) paidForPricingPhase.getPriceAmountMicros() / 10_000) + .priceAsDouble(paidForPricingPhase.getPriceAmountMicros() / 1_000_000.0) + ; + + ProductDetails.PricingPhase freeTrialSubscriptionPhase = getFreeTrialSubscriptionPhase(details.getPricingPhases()); + + if (freeTrialSubscriptionPhase != null) { + builder.freeTrialPeriod(convertToFreeTrialPeriod(freeTrialSubscriptionPhase.getBillingPeriod())); + } + } + + private ProductDetails.SubscriptionOfferDetails getActiveSubscriptionOfferDetails(List subscriptionOfferDetails) { + // TODO Google Play supports multiple SubscriptionOfferDetails which we don't support. Enhancement can be added here. + return subscriptionOfferDetails.get(0); + } + + @Nullable + private ProductDetails.PricingPhase getPaidRecurringPricingPhase(ProductDetails.SubscriptionOfferDetails details) { + for(ProductDetails.PricingPhase phase : details.getPricingPhases().getPricingPhaseList()) { + if (isPaidForSubscriptionPhase(phase)) { + return phase; + } + } + return null; + } + + private static boolean isPaidForSubscriptionPhase(ProductDetails.PricingPhase phase) { + return phase.getPriceAmountMicros() > 0; + } + + @Nullable + private static ProductDetails.PricingPhase getFreeTrialSubscriptionPhase(ProductDetails.PricingPhases phases) { + for(ProductDetails.PricingPhase phase : phases.getPricingPhaseList()) { + if (isFreeTrialSubscriptionPhase(phase)) { + return phase; + } + } + return null; + } + + + /** + * TODO test that we can find free trial phase actually like this. Normally is non-recurring and free. + */ + private static boolean isFreeTrialSubscriptionPhase(ProductDetails.PricingPhase phase) { + return phase.getRecurrenceMode() == ProductDetails.RecurrenceMode.NON_RECURRING && phase.getPriceAmountMicros() == 0L; + + } + + private static void convertOneTimeProductToInformation(Information.Builder builder, ProductDetails.OneTimePurchaseOfferDetails oneTimePurchaseDetails) { + String priceString = oneTimePurchaseDetails.getFormattedPrice(); + builder .localPricing(priceString) - .priceCurrencyCode(skuDetails.getPriceCurrencyCode()) - .priceInCents((int) (skuDetails.getPriceAmountMicros() / 10_000)) - .priceAsDouble(skuDetails.getPriceAmountMicros() / 1_000_000.0) - .build(); + .priceCurrencyCode(oneTimePurchaseDetails.getPriceCurrencyCode()) + .priceInCents((int) (oneTimePurchaseDetails.getPriceAmountMicros() / 10_000)) + .priceAsDouble(oneTimePurchaseDetails.getPriceAmountMicros() / 1_000_000.0); } /** @@ -214,7 +292,7 @@ public void dispose() { // remove observer and config as well observer = null; config = null; - Gdx.app.log(TAG, "disposed observer and config"); + Gdx.app.debug(TAG, "disposed observer and config"); } if (mBillingClient != null && mBillingClient.isReady()) { mBillingClient.endConnection(); @@ -225,37 +303,83 @@ public void dispose() { @Override public void purchase(String identifier) { - SkuDetails skuDetails = skuDetailsMap.get(identifier); + ProductDetails productDetails = productDetailsMap.get(identifier); - if (skuDetails == null) { + if (productDetails == null) { observer.handlePurchaseError(new InvalidItemException(identifier)); } else { - mBillingClient.launchBillingFlow(activity, getBillingFlowParams(skuDetails).build()); + BillingResult billingResult = mBillingClient.launchBillingFlow(activity, getBillingFlowParams(productDetails).build()); + //billingResult.getResponseCode() } } /** - * @param skuDetails SKU details to set in the billing flow params. * @return The params builder to be used while launching the billing flow. */ - protected BillingFlowParams.Builder getBillingFlowParams(SkuDetails skuDetails) { - return BillingFlowParams.newBuilder().setSkuDetails(skuDetails); + protected BillingFlowParams.Builder getBillingFlowParams(ProductDetails productDetails) { + final List productDetailsParamsList; + + if (productDetails.getProductType().equals(ProductType.INAPP)) { + productDetailsParamsList = + Collections.singletonList( + BillingFlowParams.ProductDetailsParams.newBuilder() + .setProductDetails(productDetails) + .build() + ); + } + else { + List subscriptionOfferDetails = productDetails + .getSubscriptionOfferDetails(); + final String offerToken; + + if (subscriptionOfferDetails == null || subscriptionOfferDetails.isEmpty()) { + Gdx.app.error(TAG, "subscriptionOfferDetails are empty for product: " + productDetails); + offerToken = null; + } else { + offerToken = getActiveSubscriptionOfferDetails(subscriptionOfferDetails) // HOW TO SPECIFY AN ALTERNATE OFFER USING gdx-pay? + .getOfferToken(); + } + + productDetailsParamsList = + Collections.singletonList( + BillingFlowParams.ProductDetailsParams.newBuilder() + .setProductDetails(productDetails) + .setOfferToken(offerToken) + .build() + ); + } + + return BillingFlowParams.newBuilder().setProductDetailsParamsList(productDetailsParamsList); } + + + @Override public void purchaseRestore() { - Purchase.PurchasesResult purchasesResult = mBillingClient.queryPurchases(getGlobalSkuTypeFromConfig()); - int responseCode = purchasesResult.getResponseCode(); - List purchases = purchasesResult.getPurchasesList(); - - if (responseCode == BillingClient.BillingResponseCode.OK && purchases != null) { - handlePurchase(purchases, true); + final String productType; + if (this.config.hasAnyOfferWithType(OfferType.SUBSCRIPTION)) { + productType = ProductType.SUBS; } else { - Gdx.app.error(TAG, "queryPurchases failed with responseCode " + responseCode); - observer.handleRestoreError(new GdxPayException("queryPurchases failed with " + - "responseCode " + responseCode)); + productType = ProductType.INAPP; } + mBillingClient.queryPurchasesAsync( + QueryPurchasesParams.newBuilder().setProductType(productType).build(), + + new PurchasesResponseListener() { + @Override + public void onQueryPurchasesResponse(@Nonnull BillingResult billingResult, @Nonnull List list) { + int responseCode = billingResult.getResponseCode(); + if (responseCode == BillingClient.BillingResponseCode.OK) { + handlePurchase(list, true); + } else { + Gdx.app.error(TAG, "queryPurchases failed with responseCode " + responseCode); + observer.handleRestoreError(new GdxPayException("queryPurchases failed with " + + "responseCode " + responseCode)); + } + } + }); } @Override @@ -288,15 +412,17 @@ private void handlePurchase(List purchases, boolean fromRestore) { for (Purchase purchase : purchases) { // ignore pending purchases, just return successful purchases to the client if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) { + //TODO - Only handling one product per purchase. This can be changed at Play Store - In app Product Settings + String product = purchase.getProducts().get(0); // build the transaction from the purchase object Transaction transaction = new Transaction(); - transaction.setIdentifier(purchase.getSku()); + transaction.setIdentifier(product); transaction.setOrderId(purchase.getOrderId()); transaction.setRequestId(purchase.getPurchaseToken()); transaction.setStoreName(PurchaseManagerConfig.STORE_NAME_ANDROID_GOOGLE); transaction.setPurchaseTime(new Date(purchase.getPurchaseTime())); - transaction.setPurchaseText("Purchased: " + purchase.getSku()); + transaction.setPurchaseText("Purchased: " + product); transaction.setReversalTime(null); transaction.setReversalText(null); transaction.setTransactionData(purchase.getOriginalJson()); @@ -309,7 +435,7 @@ private void handlePurchase(List purchases, boolean fromRestore) { else observer.handlePurchase(transaction); - Offer purchasedOffer = config.getOffer(purchase.getSku()); + Offer purchasedOffer = config.getOffer(product); if (purchasedOffer != null) { // CONSUMABLES need to get consumed, ENTITLEMENTS/SUBSCRIPTIONS need to geed acknowledged switch (purchasedOffer.getType()) { diff --git a/gdx-pay-android-huawei/build.gradle b/gdx-pay-android-huawei/build.gradle index f836165a..82b384db 100644 --- a/gdx-pay-android-huawei/build.gradle +++ b/gdx-pay-android-huawei/build.gradle @@ -3,8 +3,6 @@ apply plugin: 'com.android.library' apply from : '../publish_android.gradle' android { - defaultPublishConfig "release" - compileSdkVersion androidCompileSdkVersion buildToolsVersion androidBuildToolsVersion @@ -29,7 +27,7 @@ android { configurations { compileAndIncludeClassesInLibraryJar - compile.extendsFrom compileAndIncludeClassesInLibraryJar + api.extendsFrom compileAndIncludeClassesInLibraryJar } repositories { diff --git a/gdx-pay-client/build.gradle b/gdx-pay-client/build.gradle index 71f93ede..1fb9b912 100644 --- a/gdx-pay-client/build.gradle +++ b/gdx-pay-client/build.gradle @@ -1,8 +1,8 @@ -apply plugin : 'java' +apply plugin : 'java-library' apply from : '../publish_java.gradle' -sourceCompatibility = 1.6 -targetCompatibility = 1.6 +sourceCompatibility = 1.7 +targetCompatibility = 1.7 sourceSets { main { @@ -13,7 +13,7 @@ sourceSets { } dependencies { - compile project(':gdx-pay') - compile libraries.libgdx_core - testCompile libraries.junit + api project(':gdx-pay') + api libraries.libgdx_core + testImplementation libraries.junit } diff --git a/gdx-pay-iosrobovm-apple/build.gradle b/gdx-pay-iosrobovm-apple/build.gradle index 3d53b26f..3056ec6a 100644 --- a/gdx-pay-iosrobovm-apple/build.gradle +++ b/gdx-pay-iosrobovm-apple/build.gradle @@ -1,13 +1,13 @@ -apply plugin : 'java' +apply plugin : 'java-library' apply plugin : 'robovm' apply from : '../publish_java.gradle' -sourceCompatibility = 1.6 -targetCompatibility = 1.6 +sourceCompatibility = 1.7 +targetCompatibility = 1.7 dependencies { - compile project(':gdx-pay-client') - compile libraries.robovm_rt - compile libraries.robovm_cocoatouch + api project(':gdx-pay-client') + api libraries.robovm_rt + api libraries.robovm_cocoatouch } diff --git a/gdx-pay-iosrobovm-apple/src/main/java/com/badlogic/gdx/pay/ios/apple/PurchaseManageriOSApple.java b/gdx-pay-iosrobovm-apple/src/main/java/com/badlogic/gdx/pay/ios/apple/PurchaseManageriOSApple.java index 41af25a1..f21b869e 100644 --- a/gdx-pay-iosrobovm-apple/src/main/java/com/badlogic/gdx/pay/ios/apple/PurchaseManageriOSApple.java +++ b/gdx-pay-iosrobovm-apple/src/main/java/com/badlogic/gdx/pay/ios/apple/PurchaseManageriOSApple.java @@ -24,6 +24,7 @@ import javax.annotation.Nullable; import java.util.ArrayList; +import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -208,7 +209,7 @@ Transaction transaction (SKPaymentTransaction t) { transaction.setStoreName(PurchaseManagerConfig.STORE_NAME_IOS_APPLE); transaction.setOrderId(getOriginalTxID(t)); - transaction.setPurchaseTime(t.getTransactionDate().toDate()); + transaction.setPurchaseTime(t.getTransactionDate() != null ? t.getTransactionDate().toDate() : new Date()); if (product != null) { // if we didn't load product information, product will be 'null' (we only set if available) transaction.setPurchaseText("Purchased: " + product.getLocalizedTitle()); diff --git a/gdx-pay-server/build.gradle b/gdx-pay-server/build.gradle index dec49ae6..271bb70f 100644 --- a/gdx-pay-server/build.gradle +++ b/gdx-pay-server/build.gradle @@ -1,8 +1,8 @@ -apply plugin : 'java' +apply plugin : 'java-library' apply from : '../publish_java.gradle' -sourceCompatibility = 1.6 -targetCompatibility = 1.6 +sourceCompatibility = 1.7 +targetCompatibility = 1.7 sourceSets { main { @@ -13,5 +13,5 @@ sourceSets { } dependencies { - compile project(':gdx-pay') + api project(':gdx-pay') } diff --git a/gdx-pay-server/src/com/badlogic/gdx/pay/server/impl/Security.java b/gdx-pay-server/src/com/badlogic/gdx/pay/server/impl/Security.java index 4a7cf7c1..4b6807dd 100644 --- a/gdx-pay-server/src/com/badlogic/gdx/pay/server/impl/Security.java +++ b/gdx-pay-server/src/com/badlogic/gdx/pay/server/impl/Security.java @@ -29,7 +29,7 @@ public class Security { */ public static PublicKey generatePublicKey(String encodedPublicKey) { try { - byte[] decodedKey = Base64Util.fromBase64(encodedPublicKey); + byte[] decodedKey = Base64Util.decode(encodedPublicKey); KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM); return keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey)); } catch (NoSuchAlgorithmException e) { @@ -66,7 +66,7 @@ public static boolean verify(PublicKey publicKey, String signedData, String sign public static boolean verify(PublicKey publicKey, String signedData, String signature, SecurityLogger logger) { byte[] signatureBytes; try { - signatureBytes = Base64Util.fromBase64(signature); + signatureBytes = Base64Util.decode(signature); Signature sig = Signature.getInstance(SIGNATURE_ALGORITHM); sig.initVerify(publicKey); sig.update(signedData.getBytes()); diff --git a/gdx-pay-server/src/com/badlogic/gdx/pay/server/util/Base64Util.java b/gdx-pay-server/src/com/badlogic/gdx/pay/server/util/Base64Util.java index 909430a5..5a979ace 100644 --- a/gdx-pay-server/src/com/badlogic/gdx/pay/server/util/Base64Util.java +++ b/gdx-pay-server/src/com/badlogic/gdx/pay/server/util/Base64Util.java @@ -1,5 +1,6 @@ package com.badlogic.gdx.pay.server.util; +import java.io.UnsupportedEncodingException; /* * Copyright 2009 Google Inc. * @@ -19,120 +20,286 @@ /** A utility to decode and encode byte arrays as Strings, using only "safe" characters. */ public final class Base64Util { - /** An array mapping size but values to the characters that will be used to represent them. Note that this is not identical to - * the set of characters used by MIME-Base64. */ - private static final char[] BASE_64_CHARS = new char[] {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', - 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', - 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', - '$', '_'}; - - /** An array mapping legal base 64 characters [a-zA-Z0-9$_] to their associated 6-bit values. The source indices will be given - * by 7-bit ASCII characters, thus the array size needs to be 128 (actually 123 would suffice for the given set of characters - * in use). */ - private static final byte[] BASE_64_CHARS_LOOKUP = new byte[128]; - - /** Initialize the base 64 encoder values. */ - static { - // Invert the mapping (i -> base64Chars[i]) - for (int i = 0; i < BASE_64_CHARS.length; i++) { - BASE_64_CHARS_LOOKUP[BASE_64_CHARS[i]] = (byte)i; + public static class CharMap { + protected final char[] encodingMap = new char[64]; + protected final byte[] decodingMap = new byte[128]; + + public CharMap (char char63, char char64) { + int i = 0; + for (char c = 'A'; c <= 'Z'; c++) { + encodingMap[i++] = c; + } + for (char c = 'a'; c <= 'z'; c++) { + encodingMap[i++] = c; + } + for (char c = '0'; c <= '9'; c++) { + encodingMap[i++] = c; + } + encodingMap[i++] = char63; + encodingMap[i++] = char64; + for (i = 0; i < decodingMap.length; i++) { + decodingMap[i] = -1; + } + for (i = 0; i < 64; i++) { + decodingMap[encodingMap[i]] = (byte)i; + } + } + + public byte[] getDecodingMap () { + return decodingMap; + } + + public char[] getEncodingMap () { + return encodingMap; } } - private Base64Util () { - // not used + // The line separator string of the operating system. + private static final String systemLineSeparator = "\n"; + + public static final CharMap regularMap = new CharMap('+', '/'), urlsafeMap = new CharMap('-', '_'); + + /** Encodes a string into Base64 format. No blanks or line breaks are inserted. + * @param s A String to be encoded. + * @return A String containing the Base64 encoded data. */ + public static String encodeString (String s) { + return encodeString(s, false); } - /** Decode a base64 string into a byte array. - * - * @param data the encoded data. - * @return a byte array. - * @see #fromBase64(String) */ - public static byte[] fromBase64 (String data) { - if (data == null) { - return null; + /** Encodes a string into Base64 format, optionally using URL-safe encoding instead of the "regular" Base64 encoding. + * No blanks or line breaks are inserted. + * @param s A String to be encoded. + * @param useUrlsafeEncoding If true, this encodes the result with an alternate URL-safe set of characters. + * @return A String containing the Base64 encoded data. */ + public static String encodeString (String s, boolean useUrlsafeEncoding) { + try { + return new String(encode(s.getBytes("UTF-8"), useUrlsafeEncoding ? urlsafeMap.encodingMap : regularMap.encodingMap)); + } catch (UnsupportedEncodingException e) { + // shouldn't ever happen; only needed because we specify an encoding with a String + return ""; } + } - int len = data.length(); - assert (len % 4) == 0; + /** Encodes a byte array into Base64 format and breaks the output into lines of 76 characters. This method is compatible with + * sun.misc.BASE64Encoder.encodeBuffer(byte[]). + * @param in An array containing the data bytes to be encoded. + * @return A String containing the Base64 encoded data, broken into lines. */ + public static String encodeLines (byte[] in) { + return encodeLines(in, 0, in.length, 76, systemLineSeparator, regularMap.encodingMap); + } - if (len == 0) { - return new byte[0]; - } + public static String encodeLines (byte[] in, int iOff, int iLen, int lineLen, String lineSeparator, CharMap charMap) { + return encodeLines(in, iOff, iLen, lineLen, lineSeparator, charMap.encodingMap); + } - int olen = 3 * (len / 4); - if (data.charAt(len - 2) == '=') { - --olen; - } - if (data.charAt(len - 1) == '=') { - --olen; + /** Encodes a byte array into Base64 format and breaks the output into lines. + * @param in An array containing the data bytes to be encoded. + * @param iOff Offset of the first byte in in to be processed. + * @param iLen Number of bytes to be processed in in, starting at iOff. + * @param lineLen Line length for the output data. Should be a multiple of 4. + * @param lineSeparator The line separator to be used to separate the output lines. + * @param charMap char map to use + * @return A String containing the Base64 encoded data, broken into lines. */ + public static String encodeLines (byte[] in, int iOff, int iLen, int lineLen, String lineSeparator, char[] charMap) { + int blockLen = (lineLen * 3) / 4; + if (blockLen <= 0) { + throw new IllegalArgumentException(); } - byte[] bytes = new byte[olen]; - - int iidx = 0; - int oidx = 0; - while (iidx < len) { - int c0 = BASE_64_CHARS_LOOKUP[data.charAt(iidx++) & 0xff]; - int c1 = BASE_64_CHARS_LOOKUP[data.charAt(iidx++) & 0xff]; - int c2 = BASE_64_CHARS_LOOKUP[data.charAt(iidx++) & 0xff]; - int c3 = BASE_64_CHARS_LOOKUP[data.charAt(iidx++) & 0xff]; - int c24 = (c0 << 18) | (c1 << 12) | (c2 << 6) | c3; - - bytes[oidx++] = (byte)(c24 >> 16); - if (oidx == olen) { - break; - } - bytes[oidx++] = (byte)(c24 >> 8); - if (oidx == olen) { - break; - } - bytes[oidx++] = (byte)c24; + int lines = (iLen + blockLen - 1) / blockLen; + int bufLen = ((iLen + 2) / 3) * 4 + lines * lineSeparator.length(); + StringBuilder buf = new StringBuilder(bufLen); + int ip = 0; + while (ip < iLen) { + int l = Math.min(iLen - ip, blockLen); + buf.append(encode(in, iOff + ip, l, charMap)); + buf.append(lineSeparator); + ip += l; } - return bytes; - } - - /** Converts a byte array into a base 64 encoded string. Null is encoded as null, and an empty array is encoded as an empty - * string. Otherwise, the byte data is read 3 bytes at a time, with bytes off the end of the array padded with zeros. Each - * 24-bit chunk is encoded as 4 characters from the sequence [A-Za-z0-9$_]. If one of the source positions consists entirely of - * padding zeros, an '=' character is used instead. - * - * @param data a byte array, which may be null or empty - * @return a String */ - public static String toBase64 (byte[] data) { - if (data == null) { - return null; + return buf.toString(); + } + + /** Encodes a byte array into Base64 format. No blanks or line breaks are inserted in the output. + * @param in An array containing the data bytes to be encoded. + * @return A character array containing the Base64 encoded data. */ + public static char[] encode (byte[] in) { + return encode(in, regularMap.encodingMap); + } + + public static char[] encode (byte[] in, CharMap charMap) { + return encode(in, 0, in.length, charMap); + } + + public static char[] encode (byte[] in, char[] charMap) { + return encode(in, 0, in.length, charMap); + } + + /** Encodes a byte array into Base64 format. No blanks or line breaks are inserted in the output. + * @param in An array containing the data bytes to be encoded. + * @param iLen Number of bytes to process in in. + * @return A character array containing the Base64 encoded data. */ + public static char[] encode (byte[] in, int iLen) { + return encode(in, 0, iLen, regularMap.encodingMap); + } + + public static char[] encode (byte[] in, int iOff, int iLen, CharMap charMap) { + return encode(in, iOff, iLen, charMap.encodingMap); + } + + /** Encodes a byte array into Base64 format. No blanks or line breaks are inserted in the output. + * @param in An array containing the data bytes to be encoded. + * @param iOff Offset of the first byte in in to be processed. + * @param iLen Number of bytes to process in in, starting at iOff. + * @param charMap char map to use + * @return A character array containing the Base64 encoded data. */ + public static char[] encode (byte[] in, int iOff, int iLen, char[] charMap) { + int oDataLen = (iLen * 4 + 2) / 3; // output length without padding + int oLen = ((iLen + 2) / 3) * 4; // output length including padding + char[] out = new char[oLen]; + int ip = iOff; + int iEnd = iOff + iLen; + int op = 0; + while (ip < iEnd) { + int i0 = in[ip++] & 0xff; + int i1 = ip < iEnd ? in[ip++] & 0xff : 0; + int i2 = ip < iEnd ? in[ip++] & 0xff : 0; + int o0 = i0 >>> 2; + int o1 = ((i0 & 3) << 4) | (i1 >>> 4); + int o2 = ((i1 & 0xf) << 2) | (i2 >>> 6); + int o3 = i2 & 0x3F; + out[op++] = charMap[o0]; + out[op++] = charMap[o1]; + out[op] = op < oDataLen ? charMap[o2] : '='; + op++; + out[op] = op < oDataLen ? charMap[o3] : '='; + op++; } + return out; + } - int len = data.length; - if (len == 0) { - return ""; + /** Decodes a string from Base64 format. No blanks or line breaks are allowed within the Base64 encoded input data. + * @param s A Base64 String to be decoded. + * @return A String containing the decoded data. + * @throws IllegalArgumentException If the input is not valid Base64 encoded data. */ + public static String decodeString (String s) { + return decodeString(s, false); + } + + public static String decodeString (String s, boolean useUrlSafeEncoding) { + return new String(decode(s.toCharArray(), useUrlSafeEncoding ? urlsafeMap.decodingMap : regularMap.decodingMap)); + } + + public static byte[] decodeLines (String s) { + return decodeLines(s, regularMap.decodingMap); + } + + public static byte[] decodeLines (String s, CharMap inverseCharMap) { + return decodeLines(s, inverseCharMap.decodingMap); + } + + /** Decodes a byte array from Base64 format and ignores line separators, tabs and blanks. CR, LF, Tab and Space characters are + * ignored in the input data. This method is compatible with sun.misc.BASE64Decoder.decodeBuffer(String). + * @param s A Base64 String to be decoded. + * @param inverseCharMap + * @return An array containing the decoded data bytes. + * @throws IllegalArgumentException If the input is not valid Base64 encoded data. */ + public static byte[] decodeLines (String s, byte[] inverseCharMap) { + char[] buf = new char[s.length()]; + int p = 0; + for (int ip = 0; ip < s.length(); ip++) { + char c = s.charAt(ip); + if (c != ' ' && c != '\r' && c != '\n' && c != '\t') { + buf[p++] = c; + } } + return decode(buf, 0, p, inverseCharMap); + } + + /** Decodes a byte array from Base64 format. No blanks or line breaks are allowed within the Base64 encoded input data. + * @param s A Base64 String to be decoded. + * @return An array containing the decoded data bytes. + * @throws IllegalArgumentException If the input is not valid Base64 encoded data. */ + public static byte[] decode (String s) { + return decode(s.toCharArray()); + } + + /** Decodes a byte array from Base64 format. No blanks or line breaks are allowed within the Base64 encoded input data. + * @param s A Base64 String to be decoded. + * @param inverseCharMap + * @return An array containing the decoded data bytes. + * @throws IllegalArgumentException If the input is not valid Base64 encoded data. */ + public static byte[] decode (String s, CharMap inverseCharMap) { + return decode(s.toCharArray(), inverseCharMap); + } - int olen = 4 * ((len + 2) / 3); - char[] chars = new char[olen]; - - int iidx = 0; - int oidx = 0; - int charsLeft = len; - while (charsLeft > 0) { - int b0 = data[iidx++] & 0xff; - int b1 = (charsLeft > 1) ? data[iidx++] & 0xff : 0; - int b2 = (charsLeft > 2) ? data[iidx++] & 0xff : 0; - int b24 = (b0 << 16) | (b1 << 8) | b2; - - int c0 = (b24 >> 18) & 0x3f; - int c1 = (b24 >> 12) & 0x3f; - int c2 = (b24 >> 6) & 0x3f; - int c3 = b24 & 0x3f; - - chars[oidx++] = BASE_64_CHARS[c0]; - chars[oidx++] = BASE_64_CHARS[c1]; - chars[oidx++] = (charsLeft > 1) ? BASE_64_CHARS[c2] : '='; - chars[oidx++] = (charsLeft > 2) ? BASE_64_CHARS[c3] : '='; - - charsLeft -= 3; + public static byte[] decode (char[] in, byte[] inverseCharMap) { + return decode(in, 0, in.length, inverseCharMap); + } + + public static byte[] decode (char[] in, CharMap inverseCharMap) { + return decode(in, 0, in.length, inverseCharMap); + } + + /** Decodes a byte array from Base64 format. No blanks or line breaks are allowed within the Base64 encoded input data. + * @param in A character array containing the Base64 encoded data. + * @return An array containing the decoded data bytes. + * @throws IllegalArgumentException If the input is not valid Base64 encoded data. */ + public static byte[] decode (char[] in) { + return decode(in, 0, in.length, regularMap.decodingMap); + } + + public static byte[] decode (char[] in, int iOff, int iLen, CharMap inverseCharMap) { + return decode(in, iOff, iLen, inverseCharMap.decodingMap); + } + + /** Decodes a byte array from Base64 format. No blanks or line breaks are allowed within the Base64 encoded input data. + * @param in A character array containing the Base64 encoded data. + * @param iOff Offset of the first character in in to be processed. + * @param iLen Number of characters to process in in, starting at iOff. + * @param inverseCharMap charMap to use + * @return An array containing the decoded data bytes. + * @throws IllegalArgumentException If the input is not valid Base64 encoded data. */ + public static byte[] decode (char[] in, int iOff, int iLen, byte[] inverseCharMap) { + if (iLen % 4 != 0) { + throw new IllegalArgumentException("Length of Base64 encoded input string is not a multiple of 4."); } + while (iLen > 0 && in[iOff + iLen - 1] == '=') { + iLen--; + } + int oLen = (iLen * 3) / 4; + byte[] out = new byte[oLen]; + int ip = iOff; + int iEnd = iOff + iLen; + int op = 0; + while (ip < iEnd) { + int i0 = in[ip++]; + int i1 = in[ip++]; + int i2 = ip < iEnd ? in[ip++] : 'A'; + int i3 = ip < iEnd ? in[ip++] : 'A'; + if (i0 > 127 || i1 > 127 || i2 > 127 || i3 > 127) { + throw new IllegalArgumentException("Illegal character in Base64 encoded data."); + } + int b0 = inverseCharMap[i0]; + int b1 = inverseCharMap[i1]; + int b2 = inverseCharMap[i2]; + int b3 = inverseCharMap[i3]; + if (b0 < 0 || b1 < 0 || b2 < 0 || b3 < 0) { + throw new IllegalArgumentException("Illegal character in Base64 encoded data."); + } + int o0 = (b0 << 2) | (b1 >>> 4); + int o1 = ((b1 & 0xf) << 4) | (b2 >>> 2); + int o2 = ((b2 & 3) << 6) | b3; + out[op++] = (byte)o0; + if (op < oLen) { + out[op++] = (byte)o1; + } + if (op < oLen) { + out[op++] = (byte)o2; + } + } + return out; + } - return new String(chars); + // Dummy constructor. + private Base64Util () { } } diff --git a/gdx-pay/build.gradle b/gdx-pay/build.gradle index 8e8e400d..b2c8619d 100644 --- a/gdx-pay/build.gradle +++ b/gdx-pay/build.gradle @@ -1,14 +1,13 @@ -apply plugin: 'java' +apply plugin: 'java-library' -sourceCompatibility = 1.6 -targetCompatibility = 1.6 +sourceCompatibility = 1.7 +targetCompatibility = 1.7 -apply plugin : 'java' apply from : '../publish_java.gradle' dependencies { - compile libraries.jsr305 + api libraries.jsr305 - testCompile libraries.junit - testCompile libraries.assertj_core + testImplementation libraries.junit + testImplementation libraries.assertj_core } diff --git a/gdx-pay/src/main/java/com/badlogic/gdx/pay/PurchaseManagerConfig.java b/gdx-pay/src/main/java/com/badlogic/gdx/pay/PurchaseManagerConfig.java index 42059141..aa4db679 100644 --- a/gdx-pay/src/main/java/com/badlogic/gdx/pay/PurchaseManagerConfig.java +++ b/gdx-pay/src/main/java/com/badlogic/gdx/pay/PurchaseManagerConfig.java @@ -1,12 +1,12 @@ /******************************************************************************* * Copyright 2011 See AUTHORS file. - * + * * 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. @@ -70,6 +70,15 @@ public synchronized Offer getOffer (String identifier) { return null; } + public synchronized boolean hasAnyOfferWithType(OfferType offerType) { + for(Offer offer : offers) { + if (offer.getType() == offerType) { + return true; + } + } + return false; + } + public synchronized Offer getOfferForStore (String storeName, String identifierForStore) { // search matching offer and return it for (int i = 0; i < offers.size(); i++) { @@ -77,11 +86,11 @@ public synchronized Offer getOfferForStore (String storeName, String identifierF return offers.get(i); } } - + // no matching offer found return null; } - + public synchronized Offer getOffer (int index) { return offers.get(index); } @@ -91,7 +100,7 @@ public synchronized int getOfferCount () { } /** Adds a parameter for a store. - * + * * @param storeName The name of the store. * @param param The store parameters to use. This could be a string or byte-array etc. depending on what that store needs to * initialize. */ @@ -100,7 +109,7 @@ public synchronized void addStoreParam (String storeName, Object param) { } /** Returns parameters for a store. - * + * * @param storeName The name of the store. * @return The store parameters or null if there where none. This could be a string or byte-array etc. depending on what that * store needs to initialize. */ diff --git a/gradle.properties b/gradle.properties index 96ca7b51..dba85617 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ group=com.badlogicgames.gdxpay -version=1.3.2 +version=1.3.3 POM_DESCRIPTION=Gdx Payment library POM_NAME=libGDX Payment API POM_URL=https://github.com/libgdx/gdx-pay diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 0a847491..249e5832 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index f87745db..c0b9294b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,4 @@ -#Sat Sep 29 23:16:00 CEST 2018 -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists + +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip diff --git a/gradlew b/gradlew index 4453ccea..a69d9cb6 100755 --- a/gradlew +++ b/gradlew @@ -1,78 +1,129 @@ -#!/usr/bin/env sh +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# 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 +# +# https://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. +# ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME + # Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` +APP_BASE_NAME=${0##*/} # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum -warn ( ) { +warn () { echo "$*" -} +} >&2 -die ( ) { +die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -81,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" + JAVACMD=java which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the @@ -89,84 +140,101 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=$((i+1)) + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save ( ) { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=$(save "$@") - -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" - -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" fi +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index f9553162..f127cfd4 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,4 +1,20 @@ -@if "%DEBUG%" == "" @echo off +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -9,19 +25,22 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init +if %ERRORLEVEL% equ 0 goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -35,7 +54,7 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% @@ -45,38 +64,26 @@ echo location of your Java installation. goto fail -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/publish_android.gradle b/publish_android.gradle index 89d3fbb5..46f5fd9f 100644 --- a/publish_android.gradle +++ b/publish_android.gradle @@ -27,64 +27,13 @@ * added getRepositoryUrl(), isLocalBuild() */ -apply plugin: 'maven-publish' -apply plugin: 'maven' -apply plugin: 'signing' apply from: rootProject.file('publish_to_maven_repo.gradle') -afterEvaluate { project -> - task androidJavadocs(type: Javadoc) { - source = android.sourceSets.main.java.srcDirs - classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) - } - - task androidJavadocsJar(type: Jar, dependsOn: androidJavadocs) { - classifier = 'javadoc' - from androidJavadocs.destinationDir - } - - task androidSourcesJar(type: Jar) { - classifier = 'sources' - from android.sourceSets.main.java.sourceFiles - } - - artifacts { - archives androidSourcesJar - archives androidJavadocsJar - } -} - -publishing { - publications { - maven(MavenPublication) { - artifact "${project.buildDir}/outputs/aar/${project.name}-release.aar" - artifactId = project.name - groupId = project.group - version = project.version - - //The publication doesn't know about our dependencies, so we have to manually add them to the pom - // See also: http://stackoverflow.com/questions/28827593/android-library-dependencies-missing-from-pom-with-gradle - pom.withXml { - // for dependencies and exclusions - def dependenciesNode = asNode().appendNode('dependencies') - configurations.compile.allDependencies.each { ModuleDependency dp -> - def dependencyNode = dependenciesNode.appendNode('dependency') - dependencyNode.appendNode('groupId', dp.group) - dependencyNode.appendNode('artifactId', dp.name) - dependencyNode.appendNode('version', dp.version) - - // for exclusions - if (dp.excludeRules.size() > 0) { - def exclusions = dependencyNode.appendNode('exclusions') - dp.excludeRules.each { ExcludeRule ex -> - def exclusion = exclusions.appendNode('exclusion') - exclusion.appendNode('groupId', ex.group) - exclusion.appendNode('artifactId', ex.module) - } - } - } - } +android { + publishing { + singleVariant("release") { + withSourcesJar() + withJavadocJar() } } - } diff --git a/publish_java.gradle b/publish_java.gradle index 80f5381c..8e419d34 100644 --- a/publish_java.gradle +++ b/publish_java.gradle @@ -1,30 +1,8 @@ -apply plugin: 'maven' -apply plugin: 'signing' apply from: rootProject.file('publish_to_maven_repo.gradle') afterEvaluate { project -> - task libraryJar(type: Jar, dependsOn:classes) { - from sourceSets.main.output.classesDir - if (configurations.findByName('compileAndIncludeClassesInLibraryJar') != null) { - from configurations.compileAndIncludeClassesInLibraryJar.collect { it.isDirectory() ? it : zipTree(it) } - } - classifier = 'library' + java { + withJavadocJar() + withSourcesJar() } - - task sourcesJar(type: Jar, dependsOn:classes) { - from sourceSets.main.allSource - classifier = 'sources' - } - - task javadocJar(type: Jar, dependsOn:javadoc) { - from javadoc.destinationDir - classifier = 'javadoc' - } - - artifacts { - archives libraryJar - archives sourcesJar - archives javadocJar - } - } diff --git a/publish_to_maven_repo.gradle b/publish_to_maven_repo.gradle index af07e61c..06b4ecbf 100644 --- a/publish_to_maven_repo.gradle +++ b/publish_to_maven_repo.gradle @@ -1,52 +1,64 @@ -afterEvaluate { project -> - uploadArchives { - repositories { - mavenDeployer { - beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } +apply plugin: 'maven-publish' +apply plugin: 'signing' - if (isLocalBuild()) { - repository(url: getLocalRepositoryUrl()) - } else if (isReleaseBuild()) { - repository(url: getReleaseRepositoryUrl()) { - authentication(userName: getRepositoryUsername(), password: getRepositoryPassword()) - } +afterEvaluate { project -> + publishing { + publications { + release(MavenPublication) { + if (project.plugins.findPlugin("com.android.library")) { + from components.release } else { - snapshotRepository(url: getSnapshotRepositoryUrl()) { - authentication(userName: getRepositoryUsername(), password: getRepositoryPassword()) - } + from components.java } - pom.project { - name POM_NAME + pom { + name = POM_NAME if (project.hasProperty('POM_PACKAGING')) { - packaging POM_PACKAGING - } - description POM_DESCRIPTION - url POM_URL - - scm { - url POM_SCM_URL - connection POM_SCM_CONNECTION - developerConnection POM_SCM_DEV_CONNECTION + packaging = POM_PACKAGING } - + description = POM_DESCRIPTION + url = POM_URL licenses { license { - name POM_LICENCE_NAME - url POM_LICENCE_URL - distribution POM_LICENCE_DIST + name = POM_LICENCE_NAME + url = POM_LICENCE_URL + distribution = POM_LICENCE_DIST } } - developers { rootProject.developers.each { dev -> developer { - id dev.id - name dev.name - url dev.url + id = dev.id + name = dev.name + url = dev.url } } } + scm { + url = POM_SCM_URL + connection = POM_SCM_CONNECTION + developerConnection = POM_SCM_DEV_CONNECTION + } + } + } + } + + repositories { + maven { + if (isLocalBuild()) { + url = getLocalRepositoryUrl() + } else if (isReleaseBuild()) { + url = getReleaseRepositoryUrl() + credentials { + username = getRepositoryUsername() + password = getRepositoryPassword() + } + } else { + url = getSnapshotRepositoryUrl() + credentials { + username = getRepositoryUsername() + password = getRepositoryPassword() + } } } } @@ -57,6 +69,6 @@ afterEvaluate { project -> useGpgCmd() } required { isReleaseBuild() } - sign configurations.archives + sign publishing.publications } }