feat:初步搭建google隔离架构
This commit is contained in:
54
modules/module_base.gradle
Normal file
54
modules/module_base.gradle
Normal file
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* 文件说明: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"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
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 'androidx.core:core-ktx:1.7.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.4.2'
|
||||
implementation 'com.google.android.material:material: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'
|
||||
}
|
||||
|
1
modules/module_base/.gitignore
vendored
Normal file
1
modules/module_base/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/build
|
17
modules/module_base/build.gradle
Normal file
17
modules/module_base/build.gradle
Normal file
@@ -0,0 +1,17 @@
|
||||
apply from: "../module_base.gradle"
|
||||
|
||||
android {
|
||||
namespace 'com.example.module_base'
|
||||
}
|
||||
|
||||
kapt {
|
||||
arguments {
|
||||
arg("AROUTER_MODULE_NAME", project.getName())
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// 内部组件库
|
||||
api project(path: ":libs:lib_core")
|
||||
api project(path: ":libs:lib_utils")
|
||||
}
|
0
modules/module_base/consumer-rules.pro
Normal file
0
modules/module_base/consumer-rules.pro
Normal file
21
modules/module_base/proguard-rules.pro
vendored
Normal file
21
modules/module_base/proguard-rules.pro
vendored
Normal file
@@ -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
|
@@ -0,0 +1,24 @@
|
||||
package com.example.module_base
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ExampleInstrumentedTest {
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("com.example.module_base.test", appContext.packageName)
|
||||
}
|
||||
}
|
4
modules/module_base/src/main/AndroidManifest.xml
Normal file
4
modules/module_base/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
</manifest>
|
@@ -0,0 +1,9 @@
|
||||
package com.example.module_base.config
|
||||
|
||||
/**
|
||||
* Created by Max on 2023/11/22 16:01
|
||||
* Desc:
|
||||
**/
|
||||
object RouterPath {
|
||||
const val GOOGLE_SERVICE = "/google/service"
|
||||
}
|
@@ -0,0 +1,12 @@
|
||||
package com.example.module_base.support.billing
|
||||
|
||||
/**
|
||||
* Created by Max on 2023/11/22 20:55
|
||||
* Desc:
|
||||
**/
|
||||
interface IAccountIdentifiers {
|
||||
|
||||
fun getObfuscatedAccountId(): String?
|
||||
|
||||
fun getObfuscatedProfileId(): String?
|
||||
}
|
@@ -0,0 +1,12 @@
|
||||
package com.example.module_base.support.billing
|
||||
|
||||
|
||||
/**
|
||||
* Created by Max on 2023/11/22 20:20
|
||||
* Desc:
|
||||
**/
|
||||
interface IBillingResult {
|
||||
fun getResponseCode(): Int
|
||||
|
||||
fun isResponseOk(): Boolean
|
||||
}
|
@@ -0,0 +1,34 @@
|
||||
package com.example.module_base.support.billing
|
||||
|
||||
/**
|
||||
* Created by Max on 2023/11/22 17:44
|
||||
* Desc:支付
|
||||
**/
|
||||
interface IBillingService {
|
||||
|
||||
fun isServiceConnected(): Boolean
|
||||
|
||||
fun onQueryPurchases()
|
||||
|
||||
fun querySkuDetailsAsync(
|
||||
productIdList: List<String>,
|
||||
listener: IBillingService.ProductDetailsResponseListener
|
||||
)
|
||||
|
||||
fun consumeAsync(purchaseToken: String)
|
||||
|
||||
fun initiatePurchaseFlow(productDetails: IProductDetails, recordId: String)
|
||||
|
||||
fun destroy()
|
||||
|
||||
interface Listener {
|
||||
fun onBillingClientSetupFinished()
|
||||
fun onPurchasesUpdated(purchases: List<IPurchase>)
|
||||
fun onConsumeFinished(token: String?, result: Int)
|
||||
fun onFailedHandle(result: Int)
|
||||
}
|
||||
|
||||
interface ProductDetailsResponseListener {
|
||||
fun onProductDetailsResponse(billingResult: IBillingResult, productDetails: List<IProductDetails>)
|
||||
}
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
package com.example.module_base.support.billing
|
||||
|
||||
/**
|
||||
* Created by Max on 2023/11/22 21:06
|
||||
* Desc:
|
||||
**/
|
||||
interface IOneTimePurchaseOfferDetails {
|
||||
fun getPriceAmountMicros(): Long
|
||||
|
||||
fun getFormattedPrice(): String
|
||||
|
||||
fun getPriceCurrencyCode(): String
|
||||
}
|
@@ -0,0 +1,14 @@
|
||||
package com.example.module_base.support.billing
|
||||
|
||||
/**
|
||||
* Created by Max on 2023/11/22 20:17
|
||||
* Desc:
|
||||
**/
|
||||
interface IProductDetails {
|
||||
|
||||
fun getData(): Any
|
||||
|
||||
fun getProductId(): String
|
||||
|
||||
fun getOneTimePurchaseOfferDetails(): IOneTimePurchaseOfferDetails?
|
||||
}
|
@@ -0,0 +1,23 @@
|
||||
package com.example.module_base.support.billing
|
||||
|
||||
/**
|
||||
* Created by Max on 2023/11/22 19:22
|
||||
* Desc:
|
||||
**/
|
||||
interface IPurchase {
|
||||
|
||||
fun getData(): Any
|
||||
|
||||
fun getPurchaseState(): Int
|
||||
|
||||
fun isPurchasedState(): Boolean
|
||||
|
||||
fun getAccountIdentifiers(): IAccountIdentifiers?
|
||||
fun getProducts(): List<String>
|
||||
|
||||
fun getPackageName(): String
|
||||
|
||||
fun getPurchaseToken(): String
|
||||
|
||||
fun getOrderId(): String?
|
||||
}
|
@@ -0,0 +1,37 @@
|
||||
package com.example.module_base.support.google
|
||||
|
||||
import android.app.Activity
|
||||
import com.alibaba.android.arouter.facade.template.IProvider
|
||||
import com.alibaba.android.arouter.launcher.ARouter
|
||||
import com.example.module_base.support.billing.IBillingService
|
||||
import com.example.module_base.support.login.ILoginService
|
||||
|
||||
/**
|
||||
* Created by Max on 2023/11/22 15:30
|
||||
* Desc:google服务
|
||||
**/
|
||||
interface IGoogleService : IProvider {
|
||||
companion object {
|
||||
val instance: IGoogleService? by lazy {
|
||||
ARouter.getInstance().navigation(IGoogleService::class.java)
|
||||
}
|
||||
|
||||
fun newLoginService(): ILoginService? {
|
||||
return instance?.newLoginService()
|
||||
}
|
||||
|
||||
fun newBillingService(
|
||||
activity: Activity,
|
||||
listener: IBillingService.Listener
|
||||
): IBillingService? {
|
||||
return instance?.newBillingService(activity, listener)
|
||||
}
|
||||
}
|
||||
|
||||
fun newLoginService(): ILoginService
|
||||
|
||||
fun newBillingService(
|
||||
activity: Activity,
|
||||
listener: IBillingService.Listener
|
||||
): IBillingService
|
||||
}
|
@@ -0,0 +1,22 @@
|
||||
package com.example.module_base.support.login
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
|
||||
/**
|
||||
* Created by Max on 2023/11/22 15:52
|
||||
* Desc:登录服务
|
||||
**/
|
||||
interface ILoginService {
|
||||
|
||||
fun login(activity: Activity, listener: Listener)
|
||||
|
||||
fun logout()
|
||||
|
||||
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?)
|
||||
|
||||
interface Listener {
|
||||
fun onSuccess(platformInfo: PlatformInfo)
|
||||
fun onFailure(exception: LoginSDKException)
|
||||
}
|
||||
}
|
@@ -0,0 +1,21 @@
|
||||
package com.example.module_base.support.login
|
||||
|
||||
/**
|
||||
* Created by Max on 2023/11/22 17:06
|
||||
* Desc:登录SDK-异常
|
||||
**/
|
||||
class LoginSDKException : Exception {
|
||||
private var code: Int = -1
|
||||
|
||||
fun getCode(): Int {
|
||||
return code
|
||||
}
|
||||
|
||||
constructor(code: Int) : super() {
|
||||
this.code = code
|
||||
}
|
||||
|
||||
constructor(code: Int, cause: Throwable?) : super(cause) {
|
||||
this.code = code
|
||||
}
|
||||
}
|
@@ -0,0 +1,8 @@
|
||||
package com.example.module_base.support.login
|
||||
|
||||
/**
|
||||
* Created by Max on 2023/11/22 15:45
|
||||
* Desc:第三方平台信息
|
||||
**/
|
||||
data class PlatformInfo(val id: String, var name: String?, var gender: Int?, var avatar: String?) {
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
package com.example.module_base
|
||||
|
||||
import org.junit.Test
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
class ExampleUnitTest {
|
||||
@Test
|
||||
fun addition_isCorrect() {
|
||||
assertEquals(4, 2 + 2)
|
||||
}
|
||||
}
|
1
modules/module_google/.gitignore
vendored
Normal file
1
modules/module_google/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/build
|
24
modules/module_google/build.gradle
Normal file
24
modules/module_google/build.gradle
Normal file
@@ -0,0 +1,24 @@
|
||||
apply from: "../module_standard.gradle"
|
||||
android {
|
||||
namespace 'com.example.module_google'
|
||||
|
||||
defaultConfig {
|
||||
buildConfigField "String", "GOOGLE_APP_KEY", "\"xxx\""
|
||||
buildConfigField "String", "GOOGLE_SERVER_CLIENT_ID", "\"..com\""
|
||||
}
|
||||
}
|
||||
|
||||
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'
|
||||
}
|
0
modules/module_google/consumer-rules.pro
Normal file
0
modules/module_google/consumer-rules.pro
Normal file
21
modules/module_google/proguard-rules.pro
vendored
Normal file
21
modules/module_google/proguard-rules.pro
vendored
Normal file
@@ -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
|
@@ -0,0 +1,24 @@
|
||||
package com.example.module_google
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ExampleInstrumentedTest {
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("com.example.module_google.test", appContext.packageName)
|
||||
}
|
||||
}
|
4
modules/module_google/src/main/AndroidManifest.xml
Normal file
4
modules/module_google/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
</manifest>
|
@@ -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?) {
|
||||
}
|
||||
}
|
@@ -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
|
||||
}
|
||||
}
|
@@ -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
|
||||
}
|
||||
}
|
@@ -0,0 +1,231 @@
|
||||
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.ProductDetailsResponseListener
|
||||
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 com.example.module_base.support.google.IGoogleService
|
||||
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 =
|
||||
"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApNboVqeCAvW1s2mNCaNipu0bd15HC6FP86E1+dTEdMTD9XLWkYiXCzEbt+ciTda/XdAOYYe4YNnyfW7T/a9E7O0ZyjPj08q2LQaMqgTBecpzQhE7ZhYYmh3DdWrGKCEMLImX4yNuEEX9yC/dVP49nHNqfLWxmnzhDrJ5JDSlpvguOI4lMRe0/1gx7kfEeB8DOg8HfvdibiVZI4vhjk9Oz6sWJQZd1/bOuUe4huAj9Ys/4zcgniE8Da45lGdefjn12y7ELhcUZpFLLKvXmj0qnfqBX94CB+wfbEOYvALKaFb+bBZe/a8YEn/9zA4UsA0j+eRweXBb6e1AqaZJ4wp/JQIDAQAB"
|
||||
}
|
||||
|
||||
/*客户端*/
|
||||
private var billingClient: BillingClient?
|
||||
|
||||
/*是否连接成功*/
|
||||
private var isServiceConnected = false
|
||||
private set
|
||||
|
||||
/*商品列表*/
|
||||
private val purchaseList: MutableList<IPurchase> = ArrayList()
|
||||
|
||||
/*监听接口*/
|
||||
interface BillingUpdatesListener {
|
||||
fun onBillingClientSetupFinished()
|
||||
fun onPurchasesUpdated(purchases: List<Purchase>?)
|
||||
fun onConsumeFinished(token: String?, @BillingResponseCode result: Int)
|
||||
fun onFailedHandle(@BillingResponseCode result: Int)
|
||||
}
|
||||
|
||||
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<Purchase>? ->
|
||||
onPurchasesUpdated(
|
||||
billingResult,
|
||||
purchases
|
||||
)
|
||||
}
|
||||
}
|
||||
executeServiceRequest(queryToExecute)
|
||||
}
|
||||
|
||||
/*更新商品*/
|
||||
override fun onPurchasesUpdated(billingResult: BillingResult, purchases: List<Purchase>?) {
|
||||
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<String>,
|
||||
listener: IBillingService.ProductDetailsResponseListener
|
||||
) {
|
||||
val queryRequest = Runnable {
|
||||
val products = ArrayList<QueryProductDetailsParams.Product>()
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
@@ -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
|
||||
}
|
||||
}
|
@@ -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)
|
||||
}
|
||||
}
|
@@ -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<ProductDetails>) {
|
||||
listener.onProductDetailsResponse(BillingResultImpl(p0), p1.map {
|
||||
ProductDetailsImpl(it)
|
||||
})
|
||||
}
|
||||
}
|
@@ -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<String> {
|
||||
return data.products
|
||||
}
|
||||
|
||||
override fun getPackageName(): String {
|
||||
return data.packageName
|
||||
}
|
||||
|
||||
override fun getPurchaseToken(): String {
|
||||
return data.purchaseToken
|
||||
}
|
||||
|
||||
override fun getOrderId(): String? {
|
||||
return data.orderId
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,69 @@
|
||||
package com.example.module_google.login
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import com.example.lib_utils.AppUtils
|
||||
import com.example.lib_utils.log.ILog
|
||||
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, ILog {
|
||||
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 =
|
||||
GoogleSignIn.getClient(AppUtils.getApp(), googleSignInOptions)
|
||||
|
||||
override fun login(activity: Activity, listener: ILoginService.Listener) {
|
||||
this.listener = listener
|
||||
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) {
|
||||
logE("Google登录异常", e, filePrinter = true)
|
||||
listener?.onFailure(LoginSDKException(e.statusCode, e))
|
||||
} catch (e: Exception) {
|
||||
logE("Google登录异常", e, filePrinter = true)
|
||||
listener?.onFailure(LoginSDKException(-100))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
package com.example.module_google
|
||||
|
||||
import org.junit.Test
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
class ExampleUnitTest {
|
||||
@Test
|
||||
fun addition_isCorrect() {
|
||||
assertEquals(4, 2 + 2)
|
||||
}
|
||||
}
|
10
modules/module_standard.gradle
Normal file
10
modules/module_standard.gradle
Normal file
@@ -0,0 +1,10 @@
|
||||
/*
|
||||
* 文件说明:业务module的标准配置(非base模块外的业务模块)
|
||||
*/
|
||||
apply from: "../module_base.gradle"
|
||||
|
||||
dependencies {
|
||||
// Base
|
||||
implementation project(path: ":modules:module_base")
|
||||
}
|
||||
|
Reference in New Issue
Block a user