diff --git a/play-services-core-proto/src/main/proto/deviceconfig.proto b/play-services-core-proto/src/main/proto/deviceconfig.proto index 7b1954083f..a14003e662 100644 --- a/play-services-core-proto/src/main/proto/deviceconfig.proto +++ b/play-services-core-proto/src/main/proto/deviceconfig.proto @@ -73,4 +73,11 @@ message DeviceConfig { optional int32 deviceClass = 16; // unused optional int32 maxApkDownloadSizeMb = 17; + + repeated DeviceFeature deviceFeatures = 26; } + +message DeviceFeature{ + optional string name = 1; + optional int32 value = 2; +} \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/billing/AcquireFreeAppLicense.kt b/vending-app/src/main/java/org/microg/vending/billing/AcquireFreeAppLicense.kt index 8899fa1090..64cabecbec 100644 --- a/vending-app/src/main/java/org/microg/vending/billing/AcquireFreeAppLicense.kt +++ b/vending-app/src/main/java/org/microg/vending/billing/AcquireFreeAppLicense.kt @@ -3,11 +3,18 @@ package org.microg.vending.billing import android.accounts.Account import android.content.Context import android.util.Log +import io.ktor.client.plugins.ClientRequestException import io.ktor.utils.io.errors.IOException +import org.microg.gms.checkin.DeviceConfig +import org.microg.gms.checkin.DeviceFeature +import org.microg.gms.common.DeviceConfiguration +import org.microg.vending.UploadDeviceConfigRequest import org.microg.vending.billing.core.GooglePlayApi.Companion.URL_DETAILS import org.microg.vending.billing.core.GooglePlayApi.Companion.URL_PURCHASE +import org.microg.vending.billing.core.GooglePlayApi.Companion.URL_UPLOAD_DEVICE_CONFIG import org.microg.vending.billing.core.HeaderProvider import org.microg.vending.billing.core.HttpClient +import org.microg.vending.billing.proto.BuyResponse import org.microg.vending.billing.proto.GoogleApiResponse suspend fun HttpClient.acquireFreeAppLicense(context: Context, account: Account, packageName: String): Boolean { @@ -19,7 +26,7 @@ suspend fun HttpClient.acquireFreeAppLicense(context: Context, account: Account, return false } - val headers = HeaderProvider.getDefaultHeaders(authData, deviceInfo) + var headers = HeaderProvider.getDefaultHeaders(authData, deviceInfo) // Check if app is free val detailsResult = try { @@ -60,16 +67,31 @@ suspend fun HttpClient.acquireFreeAppLicense(context: Context, account: Account, "vc" to versionCode.toString() ) - val buyResult = try { - post( - url = URL_PURCHASE, - headers = headers, - params = parameters, - adapter = GoogleApiResponse.ADAPTER - ).payload?.buyResponse - } catch (e: IOException) { - Log.e(TAG, "Unable to auto-purchase $packageName because of a network error or unexpected response during purchase", e) - return false + var buyResult : BuyResponse? + try { + buyResult = purchase(headers, parameters) + } catch (e: Exception) { + Log.w(TAG, "acquireFreeAppLicense: purchase failed!", e) + if (e is ClientRequestException && e.response.status.value == 400) { + val deviceConfigResultToken = runCatching { uploadDeviceConfig(context, headers) }.getOrNull() + if (deviceConfigResultToken == null) { + Log.e(TAG, "Unable to auto-purchase $packageName because of a network error or unexpected response during device config upload") + return false + } + deviceConfigResultToken.let { + authData.deviceConfigToken = it + headers = HeaderProvider.getDefaultHeaders(authData, deviceInfo) + } + buyResult = try { + purchase(headers, parameters) + } catch (e: Exception) { + Log.e(TAG, "Unable to auto-purchase $packageName because of a network error or unexpected response during purchase", e) + return false + } + } else { + Log.e(TAG, "Unable to auto-purchase $packageName because of a network error or unexpected response during purchase", e) + return false + } } if (buyResult?.deliveryToken.isNullOrBlank()) { @@ -80,4 +102,38 @@ suspend fun HttpClient.acquireFreeAppLicense(context: Context, account: Account, } return true -} \ No newline at end of file +} + +private suspend fun HttpClient.purchase(headers: Map = emptyMap(), parameters: Map = emptyMap()) = post( + url = URL_PURCHASE, + headers = headers, + params = parameters, + adapter = GoogleApiResponse.ADAPTER +).payload?.buyResponse + + +private fun DeviceConfiguration.asProto(): DeviceConfig = DeviceConfig( + availableFeature = availableFeatures, + densityDpi = densityDpi, + glEsVersion = glEsVersion, + glExtension = glExtensions, + hasFiveWayNavigation = hasFiveWayNavigation, + hasHardKeyboard = hasHardKeyboard, + heightPixels = heightPixels, + keyboardType = keyboardType, + locale = locales, + nativePlatform = nativePlatforms, + navigation = navigation, + screenLayout = screenLayout, + sharedLibrary = sharedLibraries, + touchScreen = touchScreen, + widthPixels = widthPixels, + deviceFeatures = availableFeatures.map { name -> DeviceFeature(name, 0) } +) + +private suspend fun HttpClient.uploadDeviceConfig(context: Context, headers: Map = emptyMap()) = post( + url = URL_UPLOAD_DEVICE_CONFIG, + headers = headers, + payload = UploadDeviceConfigRequest(DeviceConfiguration(context).asProto()), + adapter = GoogleApiResponse.ADAPTER +).payload?.uploadDeviceConfigResponse?.deviceConfigToken diff --git a/vending-app/src/main/java/org/microg/vending/billing/core/AuthData.kt b/vending-app/src/main/java/org/microg/vending/billing/core/AuthData.kt index 1eb75feff8..59a76659a4 100644 --- a/vending-app/src/main/java/org/microg/vending/billing/core/AuthData.kt +++ b/vending-app/src/main/java/org/microg/vending/billing/core/AuthData.kt @@ -5,7 +5,7 @@ data class AuthData( val authToken: String, val gsfId: String = "", val deviceCheckInConsistencyToken: String = "", - val deviceConfigToken: String = "", + var deviceConfigToken: String = "", val experimentsConfigToken: String = "", val dfeCookie: String = "" ) diff --git a/vending-app/src/main/java/org/microg/vending/billing/core/GooglePlayApi.kt b/vending-app/src/main/java/org/microg/vending/billing/core/GooglePlayApi.kt index 0fadd9566f..e510da0b04 100644 --- a/vending-app/src/main/java/org/microg/vending/billing/core/GooglePlayApi.kt +++ b/vending-app/src/main/java/org/microg/vending/billing/core/GooglePlayApi.kt @@ -12,6 +12,7 @@ class GooglePlayApi { const val URL_AUTH_PROOF_TOKENS = "https://www.googleapis.com/reauth/v1beta/users/me/reauthProofTokens" const val URL_DETAILS = "$URL_FDFE/details" const val URL_ITEM_DETAILS = "$URL_FDFE/getItems" + const val URL_UPLOAD_DEVICE_CONFIG = "$URL_FDFE/uploadDeviceConfig" const val URL_PURCHASE = "$URL_FDFE/purchase" const val URL_DELIVERY = "$URL_FDFE/delivery" const val URL_ENTERPRISE_CLIENT_POLICY = "$URL_FDFE/getEnterpriseClientPolicy" diff --git a/vending-app/src/main/java/org/microg/vending/billing/core/HeaderProvider.kt b/vending-app/src/main/java/org/microg/vending/billing/core/HeaderProvider.kt index bc982f74c8..ef12993365 100644 --- a/vending-app/src/main/java/org/microg/vending/billing/core/HeaderProvider.kt +++ b/vending-app/src/main/java/org/microg/vending/billing/core/HeaderProvider.kt @@ -1,8 +1,5 @@ package org.microg.vending.billing.core -import android.util.Log -import org.microg.vending.billing.TAG - object HeaderProvider { fun getBaseHeaders(authData: AuthData, deviceInfo: DeviceEnvInfo): MutableMap { val headers: MutableMap = HashMap() @@ -38,6 +35,9 @@ object HeaderProvider { headers["X-DFE-Device-Checkin-Consistency-Token"] = authData.deviceCheckInConsistencyToken } + if (authData.deviceConfigToken.isNotBlank()) { + headers["X-DFE-Device-Config-Token"] = authData.deviceConfigToken + } return headers } } \ No newline at end of file