// Copyright Epic Games, Inc. All Rights Reserved. #include "OnlineStoreGooglePlay.h" #include "OnlineJniGooglePlayStoreHelper.h" #include "OnlineSubsystemGooglePlay.h" #include "Internationalization/Internationalization.h" #include "Internationalization/Culture.h" #include "Internationalization/FastDecimalFormat.h" FOnlineStoreGooglePlayV2::FOnlineStoreGooglePlayV2(FOnlineSubsystemGooglePlay* InSubsystem) : bIsQueryInFlight(false) , Subsystem(InSubsystem) { UE_LOG_ONLINE_STOREV2(Verbose, TEXT( "FOnlineStoreGooglePlayV2::FOnlineStoreGooglePlayV2" )); } FOnlineStoreGooglePlayV2::FOnlineStoreGooglePlayV2() : bIsQueryInFlight(false) , Subsystem(nullptr) { UE_LOG_ONLINE_STOREV2(Verbose, TEXT( "FOnlineStoreGooglePlayV2::FOnlineStoreGooglePlayV2 empty" )); } FOnlineStoreGooglePlayV2::~FOnlineStoreGooglePlayV2() { } TSharedRef ConvertProductToStoreOffer(const FOnlineStoreOffer& Product) { return MakeShared(Product); } void FOnlineStoreGooglePlayV2::OnGooglePlayAvailableIAPQueryComplete(EGooglePlayBillingResponseCode InResponseCode, const TArray& InProvidedProductInformation) { UE_LOG_ONLINE_STOREV2(Verbose, TEXT("OnGooglePlayAvailableIAPQueryComplete Response: %s NumProucts: %d"), LexToString(InResponseCode), InProvidedProductInformation.Num()); bool bSuccess = (InResponseCode == EGooglePlayBillingResponseCode::Ok); TArray OfferIds; FString ErrorStr; if(!bIsQueryInFlight) { UE_LOG_ONLINE_STOREV2(Log, TEXT("OnGooglePlayAvailableIAPQueryComplete: No IAP query in flight")); } if (bSuccess) { for (const FProvidedProductInformation& Product : InProvidedProductInformation) { TSharedRef NewProductOffer = ConvertProductToStoreOffer(Product); AddOffer(NewProductOffer); OfferIds.Add(NewProductOffer->OfferId); UE_LOG_ONLINE_STOREV2(Log, TEXT("Product Identifier: %s, Name: %s, Desc: %s, Long Desc: %s, Price: %s IntPrice: %lld"), *NewProductOffer->OfferId, *NewProductOffer->Title.ToString(), *NewProductOffer->Description.ToString(), *NewProductOffer->LongDescription.ToString(), *NewProductOffer->PriceText.ToString(), NewProductOffer->NumericPrice); } } else { ErrorStr = LexToString(InResponseCode); } QueryOnlineStoreOffersCompleteDelegate.ExecuteIfBound(bSuccess, OfferIds, ErrorStr); QueryOnlineStoreOffersCompleteDelegate.Unbind(); bIsQueryInFlight = false; } void FOnlineStoreGooglePlayV2::QueryCategories(const FUniqueNetId& UserId, const FOnQueryOnlineStoreCategoriesComplete& Delegate) { Delegate.ExecuteIfBound(false, TEXT("No CatalogService")); } void FOnlineStoreGooglePlayV2::GetCategories(TArray& OutCategories) const { OutCategories.Empty(); } void FOnlineStoreGooglePlayV2::QueryOffersByFilter(const FUniqueNetId& UserId, const FOnlineStoreFilter& Filter, const FOnQueryOnlineStoreOffersComplete& Delegate) { Delegate.ExecuteIfBound(false, TArray(), TEXT("No CatalogService")); } void FOnlineStoreGooglePlayV2::QueryOffersById(const FUniqueNetId& UserId, const TArray& OfferIds, const FOnQueryOnlineStoreOffersComplete& Delegate) { UE_LOG_ONLINE_STOREV2(Verbose, TEXT("FOnlineStoreGooglePlayV2::QueryOffersById")); if (bIsQueryInFlight) { Delegate.ExecuteIfBound(false, OfferIds, TEXT("Request already in flight")); } else if (OfferIds.Num() == 0) { Delegate.ExecuteIfBound(false, OfferIds, TEXT("No offers to query for")); } else { bIsQueryInFlight = true; QueryOnlineStoreOffersCompleteDelegate = Delegate; extern bool AndroidThunkCpp_Iap_QueryInAppPurchases(const TArray&); if (!AndroidThunkCpp_Iap_QueryInAppPurchases(OfferIds)) { Delegate.ExecuteIfBound(false, OfferIds, TEXT("Query did not start")); } } } void FOnlineStoreGooglePlayV2::AddOffer(const TSharedRef& NewOffer) { TSharedRef* Existing = CachedOffers.Find(NewOffer->OfferId); if (Existing != nullptr) { // Replace existing offer *Existing = NewOffer; } else { CachedOffers.Add(NewOffer->OfferId, NewOffer); } } void FOnlineStoreGooglePlayV2::GetOffers(TArray& OutOffers) const { for (const auto& CachedEntry : CachedOffers) { const TSharedRef& CachedOffer = CachedEntry.Value; OutOffers.Add(CachedOffer); } } TSharedPtr FOnlineStoreGooglePlayV2::GetOffer(const FUniqueOfferId& OfferId) const { TSharedPtr Result; const TSharedRef* Existing = CachedOffers.Find(OfferId); if (Existing != nullptr) { Result = (*Existing); } return Result; } void JNICALL UE::Jni::FGooglePlayStoreHelper::NativeQueryComplete(JNIEnv* jenv, jobject /*Thiz*/, jint ResponseCode, Java::Lang::TArray* ProductIDs, Java::Lang::TArray* Titles, Java::Lang::TArray* Descriptions, Java::Lang::TArray* Prices, Java::Lang::TArray* PriceValuesRaw, Java::Lang::TArray* CurrencyCodes) { TArray ProvidedProductInformation; EGooglePlayBillingResponseCode EGPResponse = (EGooglePlayBillingResponseCode)ResponseCode; bool bWasSuccessful = (EGPResponse == EGooglePlayBillingResponseCode::Ok); if (jenv && bWasSuccessful) { jsize NumProducts = jenv->GetArrayLength(ProductIDs); jsize NumTitles = jenv->GetArrayLength(Titles); jsize NumDescriptions = jenv->GetArrayLength(Descriptions); jsize NumPrices = jenv->GetArrayLength(Prices); jsize NumPricesRaw = jenv->GetArrayLength(PriceValuesRaw); jsize NumCurrencyCodes = jenv->GetArrayLength(CurrencyCodes); ensure((NumProducts == NumTitles) && (NumProducts == NumDescriptions) && (NumProducts == NumPrices) && (NumProducts == NumPricesRaw) && (NumProducts == NumCurrencyCodes)); jlong* PriceValues = jenv->GetLongArrayElements(PriceValuesRaw, 0); for (jsize Idx = 0; Idx < NumProducts; Idx++) { FOnlineStoreOffer NewProductInfo; NewProductInfo.OfferId = FJavaHelper::FStringFromLocalRef(jenv, (jstring)jenv->GetObjectArrayElement(ProductIDs, Idx)); int32 OpenParenIdx = -1; int32 CloseParenIdx = -1; FString Title = FJavaHelper::FStringFromLocalRef(jenv, (jstring)jenv->GetObjectArrayElement(Titles, Idx)); if (Title.FindLastChar(TEXT(')'), CloseParenIdx) && Title.FindLastChar(TEXT('('), OpenParenIdx) && (OpenParenIdx < CloseParenIdx)) { Title = Title.Left(OpenParenIdx).TrimEnd(); } NewProductInfo.Title = FText::FromString(Title); NewProductInfo.Description = FText::FromString(FJavaHelper::FStringFromLocalRef(jenv, (jstring)jenv->GetObjectArrayElement(Descriptions, Idx))); NewProductInfo.PriceText = FText::FromString(FJavaHelper::FStringFromLocalRef(jenv, (jstring)jenv->GetObjectArrayElement(Prices, Idx))); NewProductInfo.CurrencyCode = FJavaHelper::FStringFromLocalRef(jenv, (jstring)jenv->GetObjectArrayElement(CurrencyCodes, Idx)); // Convert the backend stated price into its base units FInternationalization& I18N = FInternationalization::Get(); const FCulture& Culture = *I18N.GetCurrentCulture(); const FDecimalNumberFormattingRules& FormattingRules = Culture.GetCurrencyFormattingRules(NewProductInfo.CurrencyCode); const FNumberFormattingOptions& FormattingOptions = FormattingRules.CultureDefaultFormattingOptions; // Value provided in PriceValues[idx] by Google Billing Library is multiplied by 1'000'000 (it is a value in micro units). We need to divide it by 10^6 and multiply by // 10^FormattingOptions.MaximumFractionalDigits to properly adjust the value to the number of fractional digits expected double Val = static_cast(PriceValues[Idx]) * static_cast(FMath::Pow(10.0f, FormattingOptions.MaximumFractionalDigits - 6)); NewProductInfo.NumericPrice = FMath::TruncToInt(Val + 0.5); NewProductInfo.ReleaseDate = FDateTime::MinValue(); NewProductInfo.ExpirationDate = FDateTime::MaxValue(); ProvidedProductInformation.Add(NewProductInfo); UE_LOG_ONLINE_STOREV2(Verbose, TEXT("\nProduct Identifier: %s, Name: %s, Description: %s, Price: %s, Price Raw: %lld, Currency Code: %s\n"), *NewProductInfo.OfferId, *NewProductInfo.Title.ToString(), *NewProductInfo.Description.ToString(), *NewProductInfo.GetDisplayPrice().ToString(), NewProductInfo.NumericPrice, *NewProductInfo.CurrencyCode); } jenv->ReleaseLongArrayElements(PriceValuesRaw, PriceValues, JNI_ABORT); } UE_LOG_ONLINE_STOREV2(Verbose, TEXT("QueryOffersById result Success: %d Response: %s"), bWasSuccessful, LexToString(EGPResponse)); if (auto OnlineSubGP = static_cast(IOnlineSubsystem::Get(GOOGLEPLAY_SUBSYSTEM))) { OnlineSubGP->ExecuteNextTick([OnlineSubGP, EGPResponse, Response = MoveTemp(ProvidedProductInformation)]() { TSharedPtr StoreInt = StaticCastSharedPtr(OnlineSubGP->GetStoreV2Interface()); StoreInt->OnGooglePlayAvailableIAPQueryComplete(EGPResponse, Response); }); } }