commit 1d5b04b34be5884d72a513fd51654c64b99f730e Author: zu Date: Tue Nov 28 01:52:12 2023 +0800 peko-android-gms init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7b75b9d --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +/build +*.iml +.gradle +/local.properties +*.DS_Store +/build +/captures +.externalNativeBuild +.idea +.settings +*.apk + diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..0e86d77 --- /dev/null +++ b/build.gradle @@ -0,0 +1,32 @@ +apply from: "./module_standard.gradle" +android { + namespace 'com.example.module_google' + + def gak = project.hasProperty("GOOGLE_APP_KEY") ? GOOGLE_APP_KEY : + "Define GOOGLE_APP_KEY in gradle.properties. Or './gradlew -PGOOGLE_APP_KEY=gak_value ... taskName'" + def gsci = project.hasProperty("GOOGLE_SERVER_CLIENT_ID") ? GOOGLE_SERVER_CLIENT_ID : + "Define GOOGLE_SERVER_CLIENT_ID in gradle.properties. Or 'gradle -PGOOGLE_SERVER_CLIENT_ID=gak_value ... taskName'" + + defaultConfig { + buildConfigField "String", "GOOGLE_APP_KEY", "\"$gak\"" + buildConfigField "String", "GOOGLE_SERVER_CLIENT_ID", "\"$gsci\"" + } +} + +kapt { + arguments { + arg("AROUTER_MODULE_NAME", project.getName()) + } +} + +dependencies { + // google登录 + implementation 'com.google.android.gms:play-services-auth:20.7.0' + + // googleplay内购 + implementation 'com.google.android.gms:play-services-wallet:19.2.1' + implementation 'com.android.billingclient:billing:6.0.1' + + // fastjson + implementation "com.alibaba:fastjson:1.2.41" +} \ No newline at end of file diff --git a/consumer-rules.pro b/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/module_base.gradle b/module_base.gradle new file mode 100644 index 0000000..902afce --- /dev/null +++ b/module_base.gradle @@ -0,0 +1,53 @@ +/* + * 文件说明:module的基础配置 + */ +apply plugin: "com.android.library" +apply plugin: "org.jetbrains.kotlin.android" +apply plugin: "kotlin-android" +apply plugin: "kotlin-kapt" +apply plugin: "com.alibaba.arouter" + +android { + compileSdkVersion COMPILE_SDK_VERSION.toInteger() + defaultConfig { + minSdkVersion MIN_SDK_VERSION.toInteger() + targetSdkVersion TARGET_SDK_VERSION.toInteger() + versionCode 1 + versionName "1.0.0" + consumerProguardFiles 'proguard-rules.pro' + } + + buildTypes { + release { + minifyEnabled true + zipAlignEnabled true + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + debug { + debuggable true + minifyEnabled false + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = '11' + } + +} + +dependencies { + implementation 'com.google.android.material:material:1.8.0' + implementation 'androidx.core:core-ktx:1.9.0' + implementation 'androidx.appcompat:appcompat:1.6.1' + + // aRouter + api 'com.alibaba:arouter-api:1.4.0' + api 'com.alibaba:arouter-annotation:1.0.6' + kapt 'com.alibaba:arouter-compiler:1.5.2' +} + diff --git a/module_standard.gradle b/module_standard.gradle new file mode 100644 index 0000000..f1ddeee --- /dev/null +++ b/module_standard.gradle @@ -0,0 +1,10 @@ +/* + * 文件说明:业务module的标准配置(非base模块外的业务模块) + */ +apply from: "./module_base.gradle" + +dependencies { + // Base + implementation project(path: ":modules:module_base") +} + diff --git a/proguard-rules.pro b/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a5918e6 --- /dev/null +++ b/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/main/java/com/example/module_google/GoogleServiceImpl.kt b/src/main/java/com/example/module_google/GoogleServiceImpl.kt new file mode 100644 index 0000000..0119e3e --- /dev/null +++ b/src/main/java/com/example/module_google/GoogleServiceImpl.kt @@ -0,0 +1,32 @@ +package com.example.module_google + +import android.app.Activity +import android.content.Context +import com.alibaba.android.arouter.facade.annotation.Route +import com.example.module_base.config.RouterPath +import com.example.module_base.support.billing.IBillingService +import com.example.module_base.support.google.IGoogleService +import com.example.module_base.support.login.ILoginService +import com.example.module_google.billing.BillingService +import com.example.module_google.login.GoogleLoginService + +/** + * Created by Max on 2023/11/22 15:56 + * Desc: + **/ +@Route(path = RouterPath.GOOGLE_SERVICE) +class GoogleServiceImpl : IGoogleService { + override fun newLoginService(): ILoginService { + return GoogleLoginService() + } + + override fun newBillingService( + activity: Activity, + listener: IBillingService.Listener + ): IBillingService { + return BillingService(activity, listener) + } + + override fun init(context: Context?) { + } +} \ No newline at end of file diff --git a/src/main/java/com/example/module_google/billing/AccountIdentifiersImpl.kt b/src/main/java/com/example/module_google/billing/AccountIdentifiersImpl.kt new file mode 100644 index 0000000..166cc6d --- /dev/null +++ b/src/main/java/com/example/module_google/billing/AccountIdentifiersImpl.kt @@ -0,0 +1,18 @@ +package com.example.module_google.billing + +import com.android.billingclient.api.AccountIdentifiers +import com.example.module_base.support.billing.IAccountIdentifiers + +/** + * Created by Max on 2023/11/22 20:57 + * Desc: + **/ +data class AccountIdentifiersImpl(val data: AccountIdentifiers) : IAccountIdentifiers { + override fun getObfuscatedAccountId(): String? { + return data.obfuscatedAccountId + } + + override fun getObfuscatedProfileId(): String? { + return data.obfuscatedProfileId + } +} \ No newline at end of file diff --git a/src/main/java/com/example/module_google/billing/BillingResultImpl.kt b/src/main/java/com/example/module_google/billing/BillingResultImpl.kt new file mode 100644 index 0000000..3b08d5c --- /dev/null +++ b/src/main/java/com/example/module_google/billing/BillingResultImpl.kt @@ -0,0 +1,20 @@ +package com.example.module_google.billing + +import com.android.billingclient.api.BillingClient +import com.android.billingclient.api.BillingResult +import com.example.module_base.support.billing.IBillingResult + +/** + * Created by Max on 2023/11/22 20:20 + * Desc: + **/ +data class BillingResultImpl(val data: BillingResult) : IBillingResult { + + override fun getResponseCode(): Int { + return data.responseCode + } + + override fun isResponseOk(): Boolean { + return getResponseCode() == BillingClient.BillingResponseCode.OK + } +} \ No newline at end of file diff --git a/src/main/java/com/example/module_google/billing/BillingService.kt b/src/main/java/com/example/module_google/billing/BillingService.kt new file mode 100644 index 0000000..48ead93 --- /dev/null +++ b/src/main/java/com/example/module_google/billing/BillingService.kt @@ -0,0 +1,221 @@ +package com.example.module_google.billing + +import android.app.Activity +import android.util.Log +import com.alibaba.fastjson.JSONObject +import com.android.billingclient.api.BillingClient +import com.android.billingclient.api.BillingClient.BillingResponseCode +import com.android.billingclient.api.BillingClientStateListener +import com.android.billingclient.api.BillingFlowParams +import com.android.billingclient.api.BillingFlowParams.ProductDetailsParams +import com.android.billingclient.api.BillingResult +import com.android.billingclient.api.ConsumeParams +import com.android.billingclient.api.ProductDetails +import com.android.billingclient.api.Purchase +import com.android.billingclient.api.PurchasesUpdatedListener +import com.android.billingclient.api.QueryProductDetailsParams +import com.android.billingclient.api.QueryPurchasesParams +import com.example.module_base.support.billing.IBillingService +import com.example.module_base.support.billing.IProductDetails +import com.example.module_base.support.billing.IPurchase +import java.io.IOException + +class BillingService(/*活动*/ + private val activity: Activity, /*监听*/ + private val listener: IBillingService.Listener +) : IBillingService, PurchasesUpdatedListener { + + companion object { + private const val TAG = "BillingManager" + + /*购买key*/ + private const val BASE_64_ENCODED_PUBLIC_KEY = + "" + } + + /*客户端*/ + private var billingClient: BillingClient? + + /*是否连接成功*/ + private var isServiceConnected = false + private set + + /*商品列表*/ + private val purchaseList: MutableList = ArrayList() + + init { + billingClient = + BillingClient.newBuilder(activity).enablePendingPurchases().setListener(this).build() + startServiceConnection { + listener.onBillingClientSetupFinished() + onQueryPurchases() + } + } + + override fun isServiceConnected(): Boolean { + return isServiceConnected + } + + /*开始连接Play*/ + private fun startServiceConnection(executeOnSuccess: Runnable?) { + billingClient?.startConnection(object : BillingClientStateListener { + override fun onBillingSetupFinished(billingResult: BillingResult) { + Log.d( + TAG, + "Setup finished. Response code: " + billingResult.debugMessage + " code = " + billingResult.responseCode + ) + if (billingResult.responseCode == BillingResponseCode.OK) { + isServiceConnected = true + executeOnSuccess?.run() + } else { + isServiceConnected = true + } + } + + override fun onBillingServiceDisconnected() { + isServiceConnected = false + } + }) + } + + /*请求商品库存*/ + override fun onQueryPurchases() { + val queryToExecute = Runnable { + billingClient?.queryPurchasesAsync( + QueryPurchasesParams.newBuilder() + .setProductType(BillingClient.ProductType.INAPP) + .build() + ) { billingResult: BillingResult, purchases: List? -> + onPurchasesUpdated( + billingResult, + purchases + ) + } + } + executeServiceRequest(queryToExecute) + } + + /*更新商品*/ + override fun onPurchasesUpdated(billingResult: BillingResult, purchases: List?) { + Log.i(TAG, "billingResult.getResponseCode()==" + billingResult.responseCode) + purchaseList.clear() + if (billingResult.responseCode == BillingResponseCode.OK) { + if (purchases != null) { + for (purchase in purchases) { + handlePurchase(PurchaseImpl(purchase)) + } + } + listener.onPurchasesUpdated(purchaseList) + } else { + if (billingResult.responseCode == BillingResponseCode.USER_CANCELED) { + } else { + } + listener.onFailedHandle(billingResult.responseCode) + } + } + + /*商品处理*/ + private fun handlePurchase(purchase: PurchaseImpl) { + //验证签名数据 + if (!verifyValidSignature(purchase.data.originalJson, purchase.data.signature)) { + return + } + purchaseList.add(purchase) + } + + /*验证签名*/ + private fun verifyValidSignature(signedData: String, signature: String): Boolean { + return try { + Security.verifyPurchase(BASE_64_ENCODED_PUBLIC_KEY, signedData, signature) + } catch (e: IOException) { + Log.e(TAG, "Got an exception trying to validate a purchase: $e") + false + } + } + + /*执行服务请求*/ + private fun executeServiceRequest(runnable: Runnable) { + if (isServiceConnected) { + runnable.run() + } else { + startServiceConnection(runnable) + } + } + + /*查询内购商品详情*/ + override fun querySkuDetailsAsync( + productIdList: List, + listener: IBillingService.ProductDetailsResponseListener + ) { + val queryRequest = Runnable { + val products = ArrayList() + for (productId in productIdList) { + products.add( + QueryProductDetailsParams.Product.newBuilder() + .setProductId(productId) + .setProductType(BillingClient.ProductType.INAPP) + .build() + ) + } + val queryProductDetailsParams = QueryProductDetailsParams.newBuilder() + .setProductList(products) + .build() + billingClient?.queryProductDetailsAsync( + queryProductDetailsParams, + ProductDetailsResponseListenerAdapter(listener) + ) + } + executeServiceRequest(queryRequest) + } + + /*启动购买,订购流程*/ + override fun initiatePurchaseFlow(productDetails: IProductDetails, recordId: String) { + val details = productDetails.getData() as? ProductDetails ?: return + val purchaseFlowRequest = Runnable { + val p = ProductDetailsParams.newBuilder() + .setProductDetails(details) + .build() + val jsonObject = JSONObject() + val oneTimePurchaseOfferDetails = details.oneTimePurchaseOfferDetails + if (oneTimePurchaseOfferDetails != null) { + jsonObject["p"] = oneTimePurchaseOfferDetails.formattedPrice + jsonObject["a"] = oneTimePurchaseOfferDetails.priceAmountMicros / 10000 + jsonObject["c"] = oneTimePurchaseOfferDetails.priceCurrencyCode + } + val purchaseParams = BillingFlowParams.newBuilder() + .setObfuscatedAccountId(recordId) + .setObfuscatedProfileId(jsonObject.toJSONString()) + .setProductDetailsParamsList(java.util.List.of(p)) + .build() + val billingResult = billingClient?.launchBillingFlow(activity, purchaseParams) + Log.i( + TAG, + " initiatePurchaseFlow billingResult=" + billingResult?.responseCode + " " + billingResult?.debugMessage + ) + } + executeServiceRequest(purchaseFlowRequest) + } + + override fun consumeAsync(purchaseToken: String) { + val consumeParams = ConsumeParams.newBuilder().setPurchaseToken(purchaseToken).build() + executeServiceRequest { + billingClient?.consumeAsync(consumeParams) { billingResult: BillingResult, s: String? -> + listener.onConsumeFinished( + purchaseToken, + billingResult.responseCode + ) + } + } + } + + /** + * 销毁结算客户端并断开连接 + */ + override fun destroy() { + Log.d(TAG, "Destroying the manager.") + if (billingClient != null && billingClient?.isReady == true) { + billingClient?.endConnection() + billingClient = null + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/module_google/billing/OneTimePurchaseOfferDetailsImpl.kt b/src/main/java/com/example/module_google/billing/OneTimePurchaseOfferDetailsImpl.kt new file mode 100644 index 0000000..95dbfd4 --- /dev/null +++ b/src/main/java/com/example/module_google/billing/OneTimePurchaseOfferDetailsImpl.kt @@ -0,0 +1,23 @@ +package com.example.module_google.billing + +import com.android.billingclient.api.ProductDetails.OneTimePurchaseOfferDetails +import com.example.module_base.support.billing.IOneTimePurchaseOfferDetails + +/** + * Created by Max on 2023/11/22 21:05 + * Desc: + **/ +class OneTimePurchaseOfferDetailsImpl(val data: OneTimePurchaseOfferDetails) : + IOneTimePurchaseOfferDetails { + override fun getPriceAmountMicros(): Long { + return data.priceAmountMicros + } + + override fun getFormattedPrice(): String { + return data.formattedPrice + } + + override fun getPriceCurrencyCode(): String { + return data.priceCurrencyCode + } +} \ No newline at end of file diff --git a/src/main/java/com/example/module_google/billing/ProductDetailsImpl.kt b/src/main/java/com/example/module_google/billing/ProductDetailsImpl.kt new file mode 100644 index 0000000..bf2e5e8 --- /dev/null +++ b/src/main/java/com/example/module_google/billing/ProductDetailsImpl.kt @@ -0,0 +1,24 @@ +package com.example.module_google.billing + +import com.android.billingclient.api.ProductDetails +import com.example.module_base.support.billing.IOneTimePurchaseOfferDetails +import com.example.module_base.support.billing.IProductDetails + +/** + * Created by Max on 2023/11/22 20:12 + * Desc: + **/ +data class ProductDetailsImpl(val data: ProductDetails) : IProductDetails { + override fun getData(): Any { + return data + } + + override fun getProductId(): String { + return data.productId + } + + override fun getOneTimePurchaseOfferDetails(): IOneTimePurchaseOfferDetails? { + val info = data.oneTimePurchaseOfferDetails ?: return null + return OneTimePurchaseOfferDetailsImpl(info) + } +} \ No newline at end of file diff --git a/src/main/java/com/example/module_google/billing/ProductDetailsResponseListenerAdapter.kt b/src/main/java/com/example/module_google/billing/ProductDetailsResponseListenerAdapter.kt new file mode 100644 index 0000000..0ad3ea0 --- /dev/null +++ b/src/main/java/com/example/module_google/billing/ProductDetailsResponseListenerAdapter.kt @@ -0,0 +1,19 @@ +package com.example.module_google.billing + +import com.android.billingclient.api.BillingResult +import com.android.billingclient.api.ProductDetails +import com.android.billingclient.api.ProductDetailsResponseListener +import com.example.module_base.support.billing.IBillingService + +/** + * Created by Max on 2023/11/22 20:23 + * Desc: + **/ +class ProductDetailsResponseListenerAdapter(private val listener: IBillingService.ProductDetailsResponseListener) : + ProductDetailsResponseListener { + override fun onProductDetailsResponse(p0: BillingResult, p1: MutableList) { + listener.onProductDetailsResponse(BillingResultImpl(p0), p1.map { + ProductDetailsImpl(it) + }) + } +} \ No newline at end of file diff --git a/src/main/java/com/example/module_google/billing/PurchaseImpl.kt b/src/main/java/com/example/module_google/billing/PurchaseImpl.kt new file mode 100644 index 0000000..fc11a09 --- /dev/null +++ b/src/main/java/com/example/module_google/billing/PurchaseImpl.kt @@ -0,0 +1,44 @@ +package com.example.module_google.billing + +import com.android.billingclient.api.Purchase +import com.example.module_base.support.billing.IAccountIdentifiers +import com.example.module_base.support.billing.IPurchase + +/** + * Created by Max on 2023/11/22 20:06 + * Desc: + **/ +data class PurchaseImpl(val data: Purchase) : IPurchase { + override fun getData(): Any { + return data + } + + override fun getPurchaseState(): Int { + return data.purchaseState + } + + override fun isPurchasedState(): Boolean { + return getPurchaseState() == Purchase.PurchaseState.PURCHASED + } + + override fun getAccountIdentifiers(): IAccountIdentifiers? { + val info = data.accountIdentifiers ?: return null + return AccountIdentifiersImpl(info) + } + + override fun getProducts(): List { + return data.products + } + + override fun getPackageName(): String { + return data.packageName + } + + override fun getPurchaseToken(): String { + return data.purchaseToken + } + + override fun getOrderId(): String? { + return data.orderId + } +} \ No newline at end of file diff --git a/src/main/java/com/example/module_google/billing/Security.java b/src/main/java/com/example/module_google/billing/Security.java new file mode 100644 index 0000000..04f48aa --- /dev/null +++ b/src/main/java/com/example/module_google/billing/Security.java @@ -0,0 +1,68 @@ +package com.example.module_google.billing; + +import android.text.TextUtils; +import android.util.Base64; + +import java.io.IOException; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; + +public class Security { + private static final String TAG = "GoogleIap/Security"; + private static final String KEY_FACTORY_ALGORITHM = "RSA"; + private static final String SIGNATURE_ALGORITHM = "SHA1withRSA"; + public static boolean verifyPurchase(String base64PublicKey, String signedData, + String signature) throws IOException { + if (TextUtils.isEmpty(signedData) || TextUtils.isEmpty(base64PublicKey) + || TextUtils.isEmpty(signature)) { + return false; + } + PublicKey key = generatePublicKey(base64PublicKey); + return verify(key, signedData, signature); + } + + public static PublicKey generatePublicKey(String encodedPublicKey) throws IOException { + try { + byte[] decodedKey = Base64.decode(encodedPublicKey, Base64.DEFAULT); + KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM); + return keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey)); + } catch (NoSuchAlgorithmException e) { + // "RSA" is guaranteed to be available. + throw new RuntimeException(e); + } catch (InvalidKeySpecException e) { + String msg = "Invalid key specification: " + e; + throw new IOException(msg); + } + } + + public static boolean verify(PublicKey publicKey, String signedData, String signature) { + byte[] signatureBytes; + try { + signatureBytes = Base64.decode(signature, Base64.DEFAULT); + } catch (IllegalArgumentException e) { + return false; + } + try { + Signature signatureAlgorithm = Signature.getInstance(SIGNATURE_ALGORITHM); + signatureAlgorithm.initVerify(publicKey); + signatureAlgorithm.update(signedData.getBytes()); + if (!signatureAlgorithm.verify(signatureBytes)) { + return false; + } + return true; + } catch (NoSuchAlgorithmException e) { + // "RSA" is guaranteed to be available. + throw new RuntimeException(e); + } catch (InvalidKeyException e) { + } catch (SignatureException e) { + } + return false; + } + +} diff --git a/src/main/java/com/example/module_google/login/GoogleLoginService.kt b/src/main/java/com/example/module_google/login/GoogleLoginService.kt new file mode 100644 index 0000000..cde87c1 --- /dev/null +++ b/src/main/java/com/example/module_google/login/GoogleLoginService.kt @@ -0,0 +1,68 @@ +package com.example.module_google.login + +import android.app.Activity +import android.content.Intent +import com.example.module_base.support.login.ILoginService +import com.example.module_base.support.login.LoginSDKException +import com.example.module_base.support.login.PlatformInfo +import com.example.module_google.BuildConfig +import com.google.android.gms.auth.api.signin.GoogleSignIn +import com.google.android.gms.auth.api.signin.GoogleSignInClient +import com.google.android.gms.auth.api.signin.GoogleSignInOptions +import com.google.android.gms.common.api.ApiException +import kotlin.random.Random + +/** + * Created by Max on 2023/11/22 15:59 + * Desc:Google登录 + **/ +class GoogleLoginService : ILoginService { + private val requestCode = Random.nextInt(10000, 20000) + + private var listener: ILoginService.Listener? = null + + private var googleSignInOptions: GoogleSignInOptions = + GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) + .requestEmail() + .requestProfile() + .requestIdToken(BuildConfig.GOOGLE_SERVER_CLIENT_ID) + .build() + + private var googleSignInClient: GoogleSignInClient? = null + + override fun login(activity: Activity, listener: ILoginService.Listener) { + this.listener = listener + if (this.googleSignInClient == null) { + this.googleSignInClient = + GoogleSignIn.getClient(activity.applicationContext, googleSignInOptions) + } + activity.startActivityForResult( + googleSignInClient!!.signInIntent, + requestCode + ) + } + + override fun logout() { + googleSignInClient?.signOut() + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (requestCode == this.requestCode) { + val task = GoogleSignIn.getSignedInAccountFromIntent(data) + try { + val account = task.getResult(ApiException::class.java) + val info = PlatformInfo( + id = account!!.id!!, + name = account.displayName, + gender = null, + avatar = account.photoUrl?.toString() + ) + listener?.onSuccess(info) + } catch (e: ApiException) { + listener?.onFailure(LoginSDKException(e.statusCode, e)) + } catch (e: Exception) { + listener?.onFailure(LoginSDKException(-100)) + } + } + } +} \ No newline at end of file