Loading vending-app/src/main/java/com/android/vending/licensing/LicensingService.kt +11 −9 Original line number Diff line number Diff line Loading @@ -20,6 +20,7 @@ import com.android.volley.toolbox.Volley import kotlinx.coroutines.runBlocking import org.microg.gms.auth.AuthConstants import org.microg.gms.profile.ProfileManager.ensureInitialized import org.microg.vending.billing.acquireFreeAppLicense import org.microg.vending.billing.core.HttpClient class LicensingService : Service() { Loading Loading @@ -83,8 +84,9 @@ class LicensingService : Service() { } catch (e: Exception) { Log.w(TAG, "Remote threw an exception while returning license result ${response}") } } } else { Log.i(TAG, "Suppressed negative license result for package $packageName") } } Loading Loading @@ -126,30 +128,30 @@ class LicensingService : Service() { val accounts = accountManager.getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE) val packageManager = packageManager lateinit var lastRespone: LicenseResponse lateinit var lastResponse: LicenseResponse if (accounts.isEmpty()) { handleNoAccounts(packageName, packageManager) return null } else for (account: Account in accounts) { lastRespone = httpClient.checkLicense( lastResponse = httpClient.checkLicense( account, accountManager, androidId, packageInfo, packageName, request ) if (lastRespone.result == LICENSED) { return lastRespone; if (lastResponse.result == LICENSED) { return lastResponse; } } // Attempt to acquire license if app is free ("auto-purchase") val firstAccount = accounts[0] /* TODO if (acquireFreeAppLicense(firstAccount)) { lastRespone = httpClient.checkLicense( if (httpClient.acquireFreeAppLicense(this@LicensingService, firstAccount, packageName)) { lastResponse = httpClient.checkLicense( firstAccount, accountManager, androidId, packageInfo, packageName, request ) }*/ } return lastRespone return lastResponse } private fun handleNoAccounts(packageName: String, packageManager: PackageManager) { Loading vending-app/src/main/java/org/microg/vending/billing/AcquireFreeAppLicense.kt 0 → 100644 +82 −0 Original line number Diff line number Diff line package org.microg.vending.billing import android.accounts.Account import android.content.Context import android.util.Log import com.android.volley.VolleyError 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.HeaderProvider import org.microg.vending.billing.core.HttpClient import org.microg.vending.billing.proto.ResponseWrapper suspend fun HttpClient.acquireFreeAppLicense(context: Context, account: Account, packageName: String): Boolean { val authData = AuthManager.getAuthData(context, account) val deviceInfo = createDeviceEnvInfo(context) if (deviceInfo == null || authData == null) { Log.e(TAG, "Unable to auto-purchase $packageName when deviceInfo = $deviceInfo and authData = $authData") return false } val headers = HeaderProvider.getDefaultHeaders(authData, deviceInfo) // Check if app is free val detailsResult = try { get( url = URL_DETAILS, headers = headers, params = mapOf("doc" to packageName), adapter = ResponseWrapper.ADAPTER ).payload?.detailsResponse } catch (e: VolleyError) { Log.e(TAG, "Unable to auto-purchase $packageName because of a network error or unexpected response when gathering app data") return false } val item = detailsResult?.item val versionCode = item?.details?.appDetails?.versionCode if (detailsResult == null || versionCode == null) { Log.e(TAG, "Unable to auto-purchase $packageName because the server did not send sufficient details") return false } val offer = item.offer if (offer == null) { Log.e(TAG, "Unable to auto-purchase $packageName because the app is not being offered at the store") } val freeApp = detailsResult.item.offer?.micros == 0L if (!freeApp) { Log.e(TAG, "Unable to auto-purchase $packageName because it is not a free app") return false } // Purchase app val parameters = mapOf( "ot" to (offer?.offerType ?: 1).toString(), "doc" to packageName, "vc" to versionCode.toString() ) val buyResult = try { post( url = URL_PURCHASE, headers = headers, params = parameters, adapter = ResponseWrapper.ADAPTER ).payload?.buyResponse } catch (e: VolleyError) { Log.e(TAG, "Unable to auto-purchase $packageName because of a network error or unexpected response during purchase") return false } if (buyResult?.encodedDeliveryToken.isNullOrBlank()) { Log.e(TAG, "Auto-purchasing $packageName failed. Was the purchase rejected by the server?") return false } else { Log.i(TAG, "Auto-purchased $packageName.") } return true } No newline at end of file vending-app/src/main/java/org/microg/vending/billing/core/GooglePlayApi.kt +2 −0 Original line number Diff line number Diff line Loading @@ -10,5 +10,7 @@ class GooglePlayApi { const val URL_CONSUME_PURCHASE = "$URL_FDFE/consumePurchase" const val URL_GET_PURCHASE_HISTORY = "$URL_FDFE/inAppPurchaseHistory" 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_PURCHASE = "$URL_FDFE/purchase" } } No newline at end of file vending-app/src/main/proto/GooglePlay.proto +143 −0 Original line number Diff line number Diff line Loading @@ -275,6 +275,8 @@ message ResponseWrapper { } message Payload { DetailsResponse detailsResponse = 2; BuyResponse buyResponse = 4; ConsumePurchaseResponse consumePurchaseResponse = 30; PurchaseHistoryResponse purchaseHistoryResponse = 67; SkuDetailsResponse skuDetailsResponse = 82; Loading @@ -282,6 +284,147 @@ message Payload { AcknowledgePurchaseResponse acknowledgePurchaseResponse = 140; } message DetailsResponse { string analyticsCookie = 2; //Review userReview = 3; Item item = 4; string footerHtml = 5; bytes serverLogsCookie = 6; //DiscoveryBadge discoveryBadge = 7; bool enableReviews = 8; //Features features = 12; string detailsStreamUrl = 13; string userReviewUrl = 14; string postAcquireDetailsStreamUrl = 17; } message BuyResponse { // … string encodedDeliveryToken = 55; // … } message Item { string id = 1; string subId = 2; int32 type = 3; int32 categoryId = 4; string title = 5; string creator = 6; string descriptionHtml = 7; Offer offer = 8; // Availability availability = 9; // repeated Image image = 10; repeated Item subItem = 11; // ContainerMetadata containerMetadata = 12; DocumentDetails details = 13; // AggregateRating aggregateRating = 14; // Annotations annotations = 15; string detailsUrl = 16; string shareUrl = 17; string reviewsUrl = 18; string backendUrl = 19; string purchaseDetailsUrl = 20; bool detailsReusable = 21; string subtitle = 22; string translatedDescriptionHtml = 23; bytes serverLogsCookie = 24; // AppInfo appInfo = 25; bool mature = 26; string promotionalDescription = 27; bool availableForPreregistration = 29; // ReviewTip tip = 30; string reviewSnippetsUrl = 31; bool forceShareability = 32; bool useWishlistAsPrimaryAction = 33; string reviewQuestionsUrl = 34; string reviewSummaryUrl = 39; } message DocumentDetails { AppDetails appDetails = 1; // SubscriptionDetails subscriptionDetails = 7; } message AppDetails { string developerName = 1; int32 majorVersionNumber = 2; int32 versionCode = 3; string versionString = 4; string title = 5; string appCategory = 7; int32 contentRating = 8; int64 infoDownloadSize = 9; string permission = 10; string developerEmail = 11; string developerWebsite = 12; string infoDownload = 13; string packageName = 14; string recentChangesHtml = 15; string infoUpdatedOn = 16; // repeated FileMetadata file = 17; string appType = 18; repeated string certificateHash = 19; bool variesWithDevice = 21; // repeated CertificateSet certificateSet = 22; repeated string autoAcquireFreeAppIfHigherVersionAvailableTag = 23; bool hasInstantLink = 24; repeated string splitId = 25; bool gamepadRequired = 26; bool externallyHosted = 27; bool everExternallyHosted = 28; string installNotes = 30; int32 installLocation = 31; int32 targetSdkVersion = 32; string hasPreregistrationPromoCode = 33; // Dependencies dependencies = 34; // TestingProgramInfo testingProgramInfo = 35; // EarlyAccessInfo earlyAccessInfo = 36; // EditorChoice editorChoice = 41; string instantLink = 43; string developerAddress = 45; // Publisher publisher = 46; string categoryName = 48; int64 downloadCount = 53; string downloadLabelDisplay = 61; string inAppProduct = 67; string downloadLabelAbbreviated = 77; string downloadLabel = 78; // Compatibility compatibility = 82; } message Offer { int64 micros = 1; string currencyCode = 2; string formattedAmount = 3; repeated Offer convertedPrice = 4; bool checkoutFlowRequired = 5; int64 fullPriceMicros = 6; string formattedFullAmount = 7; int32 offerType = 8; // RentalTerms rentalTerms = 9; int64 onSaleDate = 10; repeated string promotionLabel = 11; // SubscriptionTerms subscriptionTerms = 12; string formattedName = 13; string formattedDescription = 14; bool preorder = 15; int32 onSaleDateDisplayTimeZoneOffsetMillis = 16; int32 licensedOfferType = 17; // SubscriptionContentTerms subscriptionContentTerms = 18; string offerId = 19; int64 preorderFulfillmentDisplayDate = 20; // LicenseTerms licenseTerms = 21; bool sale = 22; // VoucherTerms voucherTerms = 23; // OfferPayment offerPayment = 24; bool repeatLastPayment = 25; string buyButtonLabel = 26; bool instantPurchaseEnabled = 27; int64 saleEndTimestamp = 30; string saleMessage = 31; } message AcquireResponse { map<string, Screen> screen = 1; AcquireResult acquireResult = 3; Loading Loading
vending-app/src/main/java/com/android/vending/licensing/LicensingService.kt +11 −9 Original line number Diff line number Diff line Loading @@ -20,6 +20,7 @@ import com.android.volley.toolbox.Volley import kotlinx.coroutines.runBlocking import org.microg.gms.auth.AuthConstants import org.microg.gms.profile.ProfileManager.ensureInitialized import org.microg.vending.billing.acquireFreeAppLicense import org.microg.vending.billing.core.HttpClient class LicensingService : Service() { Loading Loading @@ -83,8 +84,9 @@ class LicensingService : Service() { } catch (e: Exception) { Log.w(TAG, "Remote threw an exception while returning license result ${response}") } } } else { Log.i(TAG, "Suppressed negative license result for package $packageName") } } Loading Loading @@ -126,30 +128,30 @@ class LicensingService : Service() { val accounts = accountManager.getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE) val packageManager = packageManager lateinit var lastRespone: LicenseResponse lateinit var lastResponse: LicenseResponse if (accounts.isEmpty()) { handleNoAccounts(packageName, packageManager) return null } else for (account: Account in accounts) { lastRespone = httpClient.checkLicense( lastResponse = httpClient.checkLicense( account, accountManager, androidId, packageInfo, packageName, request ) if (lastRespone.result == LICENSED) { return lastRespone; if (lastResponse.result == LICENSED) { return lastResponse; } } // Attempt to acquire license if app is free ("auto-purchase") val firstAccount = accounts[0] /* TODO if (acquireFreeAppLicense(firstAccount)) { lastRespone = httpClient.checkLicense( if (httpClient.acquireFreeAppLicense(this@LicensingService, firstAccount, packageName)) { lastResponse = httpClient.checkLicense( firstAccount, accountManager, androidId, packageInfo, packageName, request ) }*/ } return lastRespone return lastResponse } private fun handleNoAccounts(packageName: String, packageManager: PackageManager) { Loading
vending-app/src/main/java/org/microg/vending/billing/AcquireFreeAppLicense.kt 0 → 100644 +82 −0 Original line number Diff line number Diff line package org.microg.vending.billing import android.accounts.Account import android.content.Context import android.util.Log import com.android.volley.VolleyError 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.HeaderProvider import org.microg.vending.billing.core.HttpClient import org.microg.vending.billing.proto.ResponseWrapper suspend fun HttpClient.acquireFreeAppLicense(context: Context, account: Account, packageName: String): Boolean { val authData = AuthManager.getAuthData(context, account) val deviceInfo = createDeviceEnvInfo(context) if (deviceInfo == null || authData == null) { Log.e(TAG, "Unable to auto-purchase $packageName when deviceInfo = $deviceInfo and authData = $authData") return false } val headers = HeaderProvider.getDefaultHeaders(authData, deviceInfo) // Check if app is free val detailsResult = try { get( url = URL_DETAILS, headers = headers, params = mapOf("doc" to packageName), adapter = ResponseWrapper.ADAPTER ).payload?.detailsResponse } catch (e: VolleyError) { Log.e(TAG, "Unable to auto-purchase $packageName because of a network error or unexpected response when gathering app data") return false } val item = detailsResult?.item val versionCode = item?.details?.appDetails?.versionCode if (detailsResult == null || versionCode == null) { Log.e(TAG, "Unable to auto-purchase $packageName because the server did not send sufficient details") return false } val offer = item.offer if (offer == null) { Log.e(TAG, "Unable to auto-purchase $packageName because the app is not being offered at the store") } val freeApp = detailsResult.item.offer?.micros == 0L if (!freeApp) { Log.e(TAG, "Unable to auto-purchase $packageName because it is not a free app") return false } // Purchase app val parameters = mapOf( "ot" to (offer?.offerType ?: 1).toString(), "doc" to packageName, "vc" to versionCode.toString() ) val buyResult = try { post( url = URL_PURCHASE, headers = headers, params = parameters, adapter = ResponseWrapper.ADAPTER ).payload?.buyResponse } catch (e: VolleyError) { Log.e(TAG, "Unable to auto-purchase $packageName because of a network error or unexpected response during purchase") return false } if (buyResult?.encodedDeliveryToken.isNullOrBlank()) { Log.e(TAG, "Auto-purchasing $packageName failed. Was the purchase rejected by the server?") return false } else { Log.i(TAG, "Auto-purchased $packageName.") } return true } No newline at end of file
vending-app/src/main/java/org/microg/vending/billing/core/GooglePlayApi.kt +2 −0 Original line number Diff line number Diff line Loading @@ -10,5 +10,7 @@ class GooglePlayApi { const val URL_CONSUME_PURCHASE = "$URL_FDFE/consumePurchase" const val URL_GET_PURCHASE_HISTORY = "$URL_FDFE/inAppPurchaseHistory" 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_PURCHASE = "$URL_FDFE/purchase" } } No newline at end of file
vending-app/src/main/proto/GooglePlay.proto +143 −0 Original line number Diff line number Diff line Loading @@ -275,6 +275,8 @@ message ResponseWrapper { } message Payload { DetailsResponse detailsResponse = 2; BuyResponse buyResponse = 4; ConsumePurchaseResponse consumePurchaseResponse = 30; PurchaseHistoryResponse purchaseHistoryResponse = 67; SkuDetailsResponse skuDetailsResponse = 82; Loading @@ -282,6 +284,147 @@ message Payload { AcknowledgePurchaseResponse acknowledgePurchaseResponse = 140; } message DetailsResponse { string analyticsCookie = 2; //Review userReview = 3; Item item = 4; string footerHtml = 5; bytes serverLogsCookie = 6; //DiscoveryBadge discoveryBadge = 7; bool enableReviews = 8; //Features features = 12; string detailsStreamUrl = 13; string userReviewUrl = 14; string postAcquireDetailsStreamUrl = 17; } message BuyResponse { // … string encodedDeliveryToken = 55; // … } message Item { string id = 1; string subId = 2; int32 type = 3; int32 categoryId = 4; string title = 5; string creator = 6; string descriptionHtml = 7; Offer offer = 8; // Availability availability = 9; // repeated Image image = 10; repeated Item subItem = 11; // ContainerMetadata containerMetadata = 12; DocumentDetails details = 13; // AggregateRating aggregateRating = 14; // Annotations annotations = 15; string detailsUrl = 16; string shareUrl = 17; string reviewsUrl = 18; string backendUrl = 19; string purchaseDetailsUrl = 20; bool detailsReusable = 21; string subtitle = 22; string translatedDescriptionHtml = 23; bytes serverLogsCookie = 24; // AppInfo appInfo = 25; bool mature = 26; string promotionalDescription = 27; bool availableForPreregistration = 29; // ReviewTip tip = 30; string reviewSnippetsUrl = 31; bool forceShareability = 32; bool useWishlistAsPrimaryAction = 33; string reviewQuestionsUrl = 34; string reviewSummaryUrl = 39; } message DocumentDetails { AppDetails appDetails = 1; // SubscriptionDetails subscriptionDetails = 7; } message AppDetails { string developerName = 1; int32 majorVersionNumber = 2; int32 versionCode = 3; string versionString = 4; string title = 5; string appCategory = 7; int32 contentRating = 8; int64 infoDownloadSize = 9; string permission = 10; string developerEmail = 11; string developerWebsite = 12; string infoDownload = 13; string packageName = 14; string recentChangesHtml = 15; string infoUpdatedOn = 16; // repeated FileMetadata file = 17; string appType = 18; repeated string certificateHash = 19; bool variesWithDevice = 21; // repeated CertificateSet certificateSet = 22; repeated string autoAcquireFreeAppIfHigherVersionAvailableTag = 23; bool hasInstantLink = 24; repeated string splitId = 25; bool gamepadRequired = 26; bool externallyHosted = 27; bool everExternallyHosted = 28; string installNotes = 30; int32 installLocation = 31; int32 targetSdkVersion = 32; string hasPreregistrationPromoCode = 33; // Dependencies dependencies = 34; // TestingProgramInfo testingProgramInfo = 35; // EarlyAccessInfo earlyAccessInfo = 36; // EditorChoice editorChoice = 41; string instantLink = 43; string developerAddress = 45; // Publisher publisher = 46; string categoryName = 48; int64 downloadCount = 53; string downloadLabelDisplay = 61; string inAppProduct = 67; string downloadLabelAbbreviated = 77; string downloadLabel = 78; // Compatibility compatibility = 82; } message Offer { int64 micros = 1; string currencyCode = 2; string formattedAmount = 3; repeated Offer convertedPrice = 4; bool checkoutFlowRequired = 5; int64 fullPriceMicros = 6; string formattedFullAmount = 7; int32 offerType = 8; // RentalTerms rentalTerms = 9; int64 onSaleDate = 10; repeated string promotionLabel = 11; // SubscriptionTerms subscriptionTerms = 12; string formattedName = 13; string formattedDescription = 14; bool preorder = 15; int32 onSaleDateDisplayTimeZoneOffsetMillis = 16; int32 licensedOfferType = 17; // SubscriptionContentTerms subscriptionContentTerms = 18; string offerId = 19; int64 preorderFulfillmentDisplayDate = 20; // LicenseTerms licenseTerms = 21; bool sale = 22; // VoucherTerms voucherTerms = 23; // OfferPayment offerPayment = 24; bool repeatLastPayment = 25; string buyButtonLabel = 26; bool instantPurchaseEnabled = 27; int64 saleEndTimestamp = 30; string saleMessage = 31; } message AcquireResponse { map<string, Screen> screen = 1; AcquireResult acquireResult = 3; Loading