Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit edb9d3b1 authored by Fynn Godau's avatar Fynn Godau Committed by Jonathan Klee
Browse files

Auto-purchase free apps

parent 80a724a7
Loading
Loading
Loading
Loading
+11 −9
Original line number Diff line number Diff line
@@ -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() {
@@ -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")
            }

        }

@@ -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) {
+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
+2 −0
Original line number Diff line number Diff line
@@ -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
+143 −0
Original line number Diff line number Diff line
@@ -275,6 +275,8 @@ message ResponseWrapper {
}

message Payload {
  DetailsResponse detailsResponse = 2;
  BuyResponse buyResponse = 4;
  ConsumePurchaseResponse consumePurchaseResponse = 30;
  PurchaseHistoryResponse purchaseHistoryResponse = 67;
  SkuDetailsResponse skuDetailsResponse = 82;
@@ -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;