Repo Created

This commit is contained in:
Fr4nz D13trich 2025-11-15 17:44:12 +01:00
parent eb305e2886
commit a8c22c65db
4784 changed files with 329907 additions and 2 deletions

162
vending-app/build.gradle Normal file
View file

@ -0,0 +1,162 @@
/*
* SPDX-FileCopyrightText: 2015 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'com.squareup.wire'
android {
namespace "com.android.vending"
compileSdkVersion androidCompileSdk
buildToolsVersion "$androidBuildVersionTools"
defaultConfig {
versionName vendingAppVersionName
versionCode vendingAppVersionCode
minSdkVersion androidMinSdk
targetSdkVersion androidTargetSdk
multiDexEnabled true
}
buildTypes {
debug {
postprocessing {
removeUnusedCode true
removeUnusedResources false
obfuscate false
optimizeCode false
proguardFile 'proguard-rules.pro'
}
}
release {
postprocessing {
removeUnusedCode true
removeUnusedResources true
obfuscate false
optimizeCode true
proguardFile 'proguard-rules.pro'
}
}
}
flavorDimensions = ['target']
productFlavors {
"default" {
dimension 'target'
}
"huawei" {
dimension 'target'
versionNameSuffix "-hw"
}
"huaweilh" {
dimension 'target'
versionNameSuffix "-lh"
versionCode vendingAppVersionCode - 1000
matchingFallbacks = ['huawei']
}
}
sourceSets {
main {
java {
srcDirs += "build/generated/source/proto/main/java"
}
}
}
buildFeatures {
aidl = true
buildConfig = true
compose true
}
lintOptions {
disable 'MissingTranslation', 'GetLocales'
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = 1.8
}
composeOptions {
kotlinCompilerExtensionVersion '1.5.10'
}
}
dependencies {
implementation project(':fake-signature')
implementation project(':play-services-auth')
implementation project(':play-services-base-core')
implementation project(':play-services-core-proto')
implementation "com.squareup.wire:wire-runtime:$wireVersion"
implementation "com.squareup.wire:wire-grpc-client:$wireVersion"
implementation "com.squareup.okhttp3:okhttp:$okHttpVersion"
implementation "io.ktor:ktor-client-core:$ktorVersion"
implementation "io.ktor:ktor-client-okhttp:$ktorVersion"
implementation "androidx.webkit:webkit:$webkitVersion"
//compose
implementation platform('androidx.compose:compose-bom:2022.10.00')
implementation 'androidx.compose.ui:ui'
implementation 'androidx.compose.material3:material3'
implementation 'androidx.compose.animation:animation-graphics'
implementation 'androidx.activity:activity-compose:1.7.2'
implementation("io.coil-kt:coil-compose:2.4.0")
implementation("io.coil-kt:coil-svg:2.2.2")
implementation "com.google.android.material:material:$materialVersion"
implementation "com.google.accompanist:accompanist-systemuicontroller:0.28.0"
implementation 'androidx.compose.ui:ui-tooling-preview'
debugImplementation 'androidx.compose.ui:ui-tooling'
// Coil (image loading)
implementation("io.coil-kt:coil-compose:2.7.0")
//droidguard
implementation project(':play-services-droidguard')
implementation project(':play-services-tasks-ktx')
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutineVersion"
//androidx
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion"
implementation "androidx.core:core-ktx:$coreVersion"
implementation "androidx.appcompat:appcompat:$appcompatVersion"
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.2'
implementation "androidx.preference:preference-ktx:$preferenceVersion"
// tink
implementation "com.google.crypto.tink:tink-android:$tinkVersion"
// multidex
implementation "androidx.multidex:multidex:$multidexVersion"
}
wire {
kotlin {
javaInterop = true
}
}
if (file('user.gradle').exists()) {
apply from: 'user.gradle'
}
android.applicationVariants.all { variant ->
variant.outputs.each { output ->
output.outputFileName = variant.applicationId + "-" + variant.versionCode + variant.versionName.substring(vendingAppVersionName.length()) + ".apk"
}
}

9
vending-app/proguard-rules.pro vendored Normal file
View file

@ -0,0 +1,9 @@
# SPDX-FileCopyrightText: 2025 e foundation
# SPDX-License-Identifier: Apache-2.0
# OKHttp rules
-dontwarn okhttp3.internal.platform.**
-dontwarn org.conscrypt.**
-dontwarn org.bouncycastle.**
-dontwarn org.openjsse.**
-dontwarn org.slf4j.impl.StaticLoggerBinder

View file

@ -0,0 +1,23 @@
<!--
~ SPDX-FileCopyrightText: 2024 microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application tools:overrideLibrary="coil.svg">
<activity
android:name="org.microg.vending.ui.MainActivity"
android:exported="true"
android:theme="@style/Theme.Dialog.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.INFO" />
</intent-filter>
</activity>
</application>
</manifest>

View file

@ -0,0 +1,21 @@
<!--
~ SPDX-FileCopyrightText: 2023 microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application tools:overrideLibrary="coil.svg">
<activity-alias
android:name="org.lighthouseex.MainActivity"
android:exported="true"
android:targetActivity="org.microg.vending.ui.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>
</application>
</manifest>

View file

@ -0,0 +1,323 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ SPDX-FileCopyrightText: 2014 microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->
<manifest xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android">
<permission
android:name="com.android.vending.CHECK_LICENSE"
android:protectionLevel="normal" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission
android:name="android.permission.GET_ACCOUNTS"
android:maxSdkVersion="22" />
<uses-permission android:name="org.microg.gms.permission.READ_SETTINGS" />
<uses-permission android:name="org.microg.gms.permission.WRITE_SETTINGS" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="com.google.android.gms.permission.READ_SETTINGS" />
<uses-permission android:name="com.google.android.gms.permission.WRITE_SETTINGS" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
<uses-permission android:name="android.permission.INSTALL_PACKAGES"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.DELETE_PACKAGES"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="com.google.android.gms.auth.permission.GOOGLE_ACCOUNT_CHANGE"/>
<permission
android:description="@string/billing_permission_desc"
android:label="@string/billing_permission_label"
android:name="com.android.vending.BILLING"
android:protectionLevel="instant|normal"/>
<uses-permission
android:name="android.permission.USE_CREDENTIALS"
android:maxSdkVersion="22" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<queries>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="market" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data
android:scheme="https"
android:host="market.android.com" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data
android:scheme="https"
android:host="play.google.com" />
</intent>
</queries>
<uses-sdk tools:overrideLibrary="coil.svg, coil.compose.singleton, coil.compose.base,
androidx.compose.material.icons,com.google.accompanist.drawablepainter,androidx.compose.ui.util,
androidx.compose.ui.unit,androidx.compose.ui.text,androidx.compose.ui.graphics,androidx.compose.ui.geometry,
androidx.activity.compose,androidx.compose.runtime.saveable,
androidx.compose.material.ripple,androidx.compose.foundation.layout,androidx.compose.animation.core,
coil.singleton, coil.base, androidx.compose.material3, com.google.accompanist.systemuicontroller, androidx.compose.animation.graphics,
androidx.compose.ui.tooling.data, androidx.compose.ui.tooling.preview" />
<application
android:forceQueryable="true"
android:icon="@mipmap/ic_app"
android:roundIcon="@mipmap/ic_app"
android:label="@string/app_name"
android:name="androidx.multidex.MultiDexApplication">
<meta-data
android:name="org.microg.gms.settings:source-package"
android:value="com.google.android.gms" />
<meta-data
android:name="org.microg.gms.profile:source-package"
android:value="com.google.android.gms" />
<activity
android:name=".GrantFakeSignaturePermissionActivity"
android:exported="true"
android:theme="@style/Theme.Dialog.NoActionBar" />
<service
android:name="com.android.vending.licensing.LicensingService"
android:permission="com.android.vending.CHECK_LICENSE"
android:exported="true">
<intent-filter>
<action android:name="com.android.vending.licensing.ILicensingService" />
</intent-filter>
</service>
<service
android:name="com.google.android.finsky.externalreferrer.GetInstallReferrerService"
android:exported="true">
<intent-filter>
<action android:name="com.google.android.finsky.BIND_GET_INSTALL_REFERRER_SERVICE" />
</intent-filter>
</service>
<activity
android:name="org.microg.vending.ui.MainActivity"
android:theme="@style/Theme.Dialog.NoActionBar"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.INFO" />
</intent-filter>
</activity>
<activity-alias
android:name="com.google.android.finsky.activities.MarketDeepLinkHandlerActivity"
android:exported="true"
android:targetActivity="org.microg.vending.MarketIntentRedirect">
</activity-alias>
<activity
android:name="org.microg.vending.MarketIntentRedirect"
android:theme="@style/Theme.Dialog.NoActionBar"
android:exported="true">
<intent-filter android:priority="-100">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="market" />
</intent-filter>
<!--
Play Store website opens this via intent:// URI if BROWSABLE category is added.
If the only other valid or the configured default retriever of the intent is the web browser itself,
this would cause an infinite loop of redirects between the redirector and the web browser opening
the Play Store website.
To prevent this, we remove the BROWSABLE category. This ensure best possible compatibility without running
into the aforementioned issue.
-->
<intent-filter
android:priority="-100"
tools:ignore="AppLinkUrlError">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="play.google.com" />
<data android:host="market.android.com" />
</intent-filter>
</activity>
<receiver
android:name="com.android.vending.licensing.IgnoreReceiver"
android:exported="false" />
<receiver
android:name="com.android.vending.licensing.SignInReceiver"
android:exported="false" />
<activity
android:name="org.microg.vending.billing.PurchaseActivity"
android:exported="true"
android:theme="@style/Theme.Translucent"
android:process=":ui">
<intent-filter>
<action android:name="com.android.vending.billing.PURCHASE" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<service android:name="com.google.android.finsky.splitinstallservice.SplitInstallService"
android:exported="true">
<intent-filter>
<action android:name="com.google.android.play.core.splitinstall.BIND_SPLIT_INSTALL_SERVICE"/>
</intent-filter>
</service>
<service
android:name="com.google.android.finsky.appcontentservice.engage.AppEngageServiceV2"
android:exported="true"
android:process=":background">
<intent-filter>
<action android:name="com.google.android.engage.BIND_APP_ENGAGE_SERVICE"/>
</intent-filter>
</service>
<service
android:name="com.android.vending.billing.InAppBillingService"
android:exported="true">
<intent-filter>
<action android:name="com.android.vending.billing.InAppBillingService.BIND" />
</intent-filter>
</service>
<activity
android:name="org.microg.vending.billing.ui.InAppBillingHostActivity"
android:exported="true"
android:theme="@style/InAppBillingStyle"
android:windowSoftInputMode="adjustResize" />
<activity
android:name="org.microg.vending.billing.ui.PlayWebViewActivity"
android:process=":ui"
android:exported="false" />
<service
android:name="com.google.android.finsky.assetmoduleservice.AssetModuleService"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="com.google.android.play.core.assetmoduleservice.BIND_ASSET_MODULE_SERVICE" />
</intent-filter>
</service>
<receiver
android:name=".installer.SessionResultReceiver"
android:exported="false"
tools:targetApi="21" />
<receiver
android:name=".installer.InstallReceiver"
android:exported="false"
tools:targetApi="21" />
<!-- Work account store -->
<activity android:name="org.microg.vending.ui.WorkAppsActivity"
android:exported="true"
android:theme="@style/Theme.Material3.DayNight"
android:label="@string/vending_activity_name"
android:enabled="false"
tools:targetApi="21">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service android:name=".installer.InstallService"
android:foregroundServiceType="dataSync"
android:exported="false"
tools:targetApi="21" />
<receiver android:name="org.microg.vending.WorkAccountChangedReceiver"
android:exported="true">
<intent-filter>
<action android:name="org.microg.vending.WORK_ACCOUNT_CHANGED" />
</intent-filter>
</receiver>
<service
android:name="com.google.android.finsky.integrityservice.IntegrityService"
android:exported="true">
<intent-filter>
<action android:name="com.google.android.play.core.integrityservice.BIND_INTEGRITY_SERVICE" />
</intent-filter>
</service>
<service
android:name="com.google.android.finsky.expressintegrityservice.ExpressIntegrityService"
android:exported="true">
<intent-filter>
<action android:name="com.google.android.play.core.expressintegrityservice.BIND_EXPRESS_INTEGRITY_SERVICE"/>
</intent-filter>
</service>
<receiver
android:name="com.google.android.finsky.accounts.impl.AccountsChangedReceiver"
android:permission="com.google.android.gms.auth.permission.GOOGLE_ACCOUNT_CHANGE"
android:exported="true">
<intent-filter>
<action android:name="com.google.android.gms.auth.GOOGLE_ACCOUNT_CHANGE"/>
</intent-filter>
</receiver>
<service
android:name="com.google.android.finsky.services.PlayGearheadService"
android:exported="true" />
<service android:name="com.google.android.finsky.installservice.DevTriggeredUpdateService"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="com.google.android.play.core.install.BIND_UPDATE_SERVICE"/>
</intent-filter>
</service>
<service
android:name="com.google.android.finsky.inappreviewservice.InAppReviewService"
android:exported="true">
<intent-filter>
<action android:name="com.google.android.finsky.BIND_IN_APP_REVIEW_SERVICE"/>
</intent-filter>
</service>
<activity
android:name="org.microg.vending.installer.AppInstallActivity"
android:exported="true"
android:excludeFromRecents="true"
android:theme="@style/Theme.App.Translucent"
tools:targetApi="21">
<intent-filter>
<action android:name="org.microg.vending.action.INSTALL_PACKAGE"/>
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/vnd.android.package-archive" />
</intent-filter>
</activity>
<activity
android:name="org.microg.vending.installer.AskInstallReminderActivity"
android:exported="false"
android:excludeFromRecents="true"
android:theme="@style/Theme.AppCompat.NoActionBar"
tools:targetApi="21"/>
</application>
</manifest>

View file

@ -0,0 +1,18 @@
/*
* SPDX-FileCopyrightText: 2024 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.android.vending.billing;
import android.os.Bundle;
interface IInAppBillingCreateAlternativeBillingOnlyTokenCallback {
/**
* @param bundle a Bundle with the following keys:
* "RESPONSE_CODE" - Integer
* "DEBUG_MESSAGE" - String
* "CREATE_ALTERNATIVE_BILLING_ONLY_REPORTING_DETAILS" - String with JSON encoded reporting details with the following keys:
* "externalTransactionToken" - String used to report a transaction made via alternative billing
*/
void callback(in Bundle bundle);
}

View file

@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: 2024 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.android.vending.billing;
import android.os.Bundle;
interface IInAppBillingCreateExternalPaymentReportingDetailsCallback {
/**
* @param bundle a Bundle with the following keys:
* "RESPONSE_CODE" - Integer
* "DEBUG_MESSAGE" - String
*/
void callback(in Bundle bundle);
}

View file

@ -0,0 +1,11 @@
/*
* SPDX-FileCopyrightText: 2024 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.android.vending.billing;
import android.os.Bundle;
interface IInAppBillingDelegateToBackendCallback {
}

View file

@ -0,0 +1,17 @@
/*
* SPDX-FileCopyrightText: 2024 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.android.vending.billing;
import android.os.Bundle;
interface IInAppBillingGetAlternativeBillingOnlyDialogIntentCallback {
/**
* @param bundle a Bundle with the following keys:
* "RESPONSE_CODE" - Integer
* "DEBUG_MESSAGE" - String
* "ALTERNATIVE_BILLING_ONLY_DIALOG_INTENT" - PendingIntent
*/
void callback(in Bundle bundle);
}

View file

@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: 2023 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.android.vending.billing;
import android.os.Bundle;
interface IInAppBillingGetBillingConfigCallback {
/**
* @param bundle a Bundle with the following keys:
* "BILLING_CONFIG" - String with JSON encoded billing config with following keys:
* "countryCode" - String with customer's country code (ISO-3166-1 alpha2)
*/
void callback(in Bundle bundle);
}

View file

@ -0,0 +1,17 @@
/*
* SPDX-FileCopyrightText: 2024 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.android.vending.billing;
import android.os.Bundle;
interface IInAppBillingGetExternalPaymentDialogIntentCallback {
/**
* @param bundle a Bundle with the following keys:
* "RESPONSE_CODE" - Integer
* "DEBUG_MESSAGE" - String
* "EXTERNAL_PAYMENT_DIALOG_INTENT" - PendingIntent
*/
void callback(in Bundle bundle);
}

View file

@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: 2024 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.android.vending.billing;
import android.os.Bundle;
interface IInAppBillingIsAlternativeBillingOnlyAvailableCallback {
/**
* @param bundle a Bundle with the following keys:
* "RESPONSE_CODE" - Integer
* "DEBUG_MESSAGE" - String
*/
void callback(in Bundle bundle);
}

View file

@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: 2024 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.android.vending.billing;
import android.os.Bundle;
interface IInAppBillingIsExternalPaymentAvailableCallback {
/**
* @param bundle a Bundle with the following keys:
* "RESPONSE_CODE" - Integer
* "DEBUG_MESSAGE" - String
*/
void callback(in Bundle bundle);
}

View file

@ -0,0 +1,444 @@
package com.android.vending.billing;
import android.os.Bundle;
import com.android.vending.billing.IInAppBillingServiceCallback;
import com.android.vending.billing.IInAppBillingCreateAlternativeBillingOnlyTokenCallback;
import com.android.vending.billing.IInAppBillingCreateExternalPaymentReportingDetailsCallback;
import com.android.vending.billing.IInAppBillingDelegateToBackendCallback;
import com.android.vending.billing.IInAppBillingGetAlternativeBillingOnlyDialogIntentCallback;
import com.android.vending.billing.IInAppBillingGetBillingConfigCallback;
import com.android.vending.billing.IInAppBillingGetExternalPaymentDialogIntentCallback;
import com.android.vending.billing.IInAppBillingIsAlternativeBillingOnlyAvailableCallback;
import com.android.vending.billing.IInAppBillingIsExternalPaymentAvailableCallback;
/**
* InAppBillingService is the service that provides in-app billing version 3 and beyond.
* This service provides the following features:
* 1. Provides a new API to get details of in-app items published for the app including
* price, type, title and description.
* 2. The purchase flow is synchronous and purchase information is available immediately
* after it completes.
* 3. Purchase information of in-app purchases is maintained within the Google Play system
* till the purchase is consumed.
* 4. An API to consume a purchase of an inapp item. All purchases of one-time
* in-app items are consumable and thereafter can be purchased again.
* 5. An API to get current purchases of the user immediately. This will not contain any
* consumed purchases.
*
* All calls will give a response code with the following possible values
* RESULT_OK = 0 - success
* RESULT_USER_CANCELED = 1 - User pressed back or canceled a dialog
* RESULT_SERVICE_UNAVAILABLE = 2 - The network connection is down
* RESULT_BILLING_UNAVAILABLE = 3 - This billing API version is not supported for the type requested
* RESULT_ITEM_UNAVAILABLE = 4 - Requested SKU is not available for purchase
* RESULT_DEVELOPER_ERROR = 5 - Invalid arguments provided to the API
* RESULT_ERROR = 6 - Fatal error during the API action
* RESULT_ITEM_ALREADY_OWNED = 7 - Failure to purchase since item is already owned
* RESULT_ITEM_NOT_OWNED = 8 - Failure to consume since item is not owned
*/
interface IInAppBillingService {
/**
* Checks support for the requested billing API version, package and in-app type.
* Minimum API version supported by this interface is 3.
* @param apiVersion billing API version that the app is using
* @param packageName the package name of the calling app
* @param type type of the in-app item being purchased ("inapp" for one-time purchases
* and "subs" for subscriptions)
* @return RESULT_OK(0) on success and appropriate response code on failures.
*/
int isBillingSupported(int apiVersion, String packageName, String type) = 0;
/**
* Provides details of a list of SKUs
* Given a list of SKUs of a valid type in the skusBundle, this returns a bundle
* with a list JSON strings containing the productId, price, title and description.
* This API can be called with a maximum of 20 SKUs.
* @param apiVersion billing API version that the app is using
* @param packageName the package name of the calling app
* @param type of the in-app items ("inapp" for one-time purchases
* and "subs" for subscriptions)
* @param skusBundle bundle containing a StringArrayList of SKUs with key "ITEM_ID_LIST"
* @return Bundle containing the following key-value pairs
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response codes
* on failures.
* "DETAILS_LIST" with a StringArrayList containing purchase information
* in JSON format similar to:
* '{ "productId" : "exampleSku",
* "type" : "inapp",
* "price" : "$5.00",
* "price_currency": "USD",
* "price_amount_micros": 5000000,
* "title : "Example Title",
* "description" : "This is an example description" }'
*/
Bundle getSkuDetails(int apiVersion, String packageName, String type, in Bundle skusBundle) = 1;
/**
* Returns a pending intent to launch the purchase flow for an in-app item by providing a SKU,
* the type, a unique purchase token and an optional developer payload.
* @param apiVersion billing API version that the app is using
* @param packageName package name of the calling app
* @param sku the SKU of the in-app item as published in the developer console
* @param type of the in-app item being purchased ("inapp" for one-time purchases
* and "subs" for subscriptions)
* @param developerPayload optional argument to be sent back with the purchase information
* @return Bundle containing the following key-value pairs
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response codes
* on failures.
* "BUY_INTENT" - PendingIntent to start the purchase flow
*
* The Pending intent should be launched with startIntentSenderForResult. When purchase flow
* has completed, the onActivityResult() will give a resultCode of OK or CANCELED.
* If the purchase is successful, the result data will contain the following key-value pairs
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response
* codes on failures.
* "INAPP_PURCHASE_DATA" - String in JSON format similar to
* '{"orderId":"12999763169054705758.1371079406387615",
* "packageName":"com.example.app",
* "productId":"exampleSku",
* "purchaseTime":1345678900000,
* "purchaseToken" : "122333444455555",
* "developerPayload":"example developer payload" }'
* "INAPP_DATA_SIGNATURE" - String containing the signature of the purchase data that
* was signed with the private key of the developer
*/
Bundle getBuyIntent(int apiVersion, String packageName, String sku, String type,
String developerPayload) = 2;
/**
* Returns the current SKUs owned by the user of the type and package name specified along with
* purchase information and a signature of the data to be validated.
* This will return all SKUs that have been purchased in V3 and managed items purchased using
* V1 and V2 that have not been consumed.
* @param apiVersion billing API version that the app is using
* @param packageName package name of the calling app
* @param type of the in-app items being requested ("inapp" for one-time purchases
* and "subs" for subscriptions)
* @param continuationToken to be set as null for the first call, if the number of owned
* skus are too many, a continuationToken is returned in the response bundle.
* This method can be called again with the continuation token to get the next set of
* owned skus.
* @return Bundle containing the following key-value pairs
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response codes
on failures.
* "INAPP_PURCHASE_ITEM_LIST" - StringArrayList containing the list of SKUs
* "INAPP_PURCHASE_DATA_LIST" - StringArrayList containing the purchase information
* "INAPP_DATA_SIGNATURE_LIST"- StringArrayList containing the signatures
* of the purchase information
* "INAPP_CONTINUATION_TOKEN" - String containing a continuation token for the
* next set of in-app purchases. Only set if the
* user has more owned skus than the current list.
*/
Bundle getPurchases(int apiVersion, String packageName, String type, String continuationToken) = 3;
/**
* Consume the last purchase of the given SKU. This will result in this item being removed
* from all subsequent responses to getPurchases() and allow re-purchase of this item.
* @param apiVersion billing API version that the app is using
* @param packageName package name of the calling app
* @param purchaseToken token in the purchase information JSON that identifies the purchase
* to be consumed
* @return RESULT_OK(0) if consumption succeeded, appropriate response codes on failures.
*/
int consumePurchase(int apiVersion, String packageName, String purchaseToken) = 4;
int isPromoEligible(int apiVersion, String packageName, String type) = 5;
/**
* Returns a pending intent to launch the purchase flow for upgrading or downgrading a
* subscription. The existing owned SKU(s) should be provided along with the new SKU that
* the user is upgrading or downgrading to.
* @param apiVersion billing API version that the app is using, must be 5 or later
* @param packageName package name of the calling app
* @param oldSkus the SKU(s) that the user is upgrading or downgrading from,
* if null or empty this method will behave like {@link #getBuyIntent}
* @param newSku the SKU that the user is upgrading or downgrading to
* @param type of the item being purchased, currently must be "subs"
* @param developerPayload optional argument to be sent back with the purchase information
* @return Bundle containing the following key-value pairs
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response codes
* on failures.
* "BUY_INTENT" - PendingIntent to start the purchase flow
*
* The Pending intent should be launched with startIntentSenderForResult. When purchase flow
* has completed, the onActivityResult() will give a resultCode of OK or CANCELED.
* If the purchase is successful, the result data will contain the following key-value pairs
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response
* codes on failures.
* "INAPP_PURCHASE_DATA" - String in JSON format similar to
* '{"orderId":"12999763169054705758.1371079406387615",
* "packageName":"com.example.app",
* "productId":"exampleSku",
* "purchaseTime":1345678900000,
* "purchaseToken" : "122333444455555",
* "developerPayload":"example developer payload" }'
* "INAPP_DATA_SIGNATURE" - String containing the signature of the purchase data that
* was signed with the private key of the developer
*/
Bundle getBuyIntentToReplaceSkus(int apiVersion, String packageName,
in List<String> oldSkus, String newSku, String type, String developerPayload) = 6;
/**
* Returns a pending intent to launch the purchase flow for an in-app item. This method is
* a variant of the {@link #getBuyIntent} method and takes an additional {@code extraParams}
* parameter. This parameter is a Bundle of optional keys and values that affect the
* operation of the method.
* @param apiVersion billing API version that the app is using, must be 6 or later
* @param packageName package name of the calling app
* @param sku the SKU of the in-app item as published in the developer console
* @param type of the in-app item being purchased ("inapp" for one-time purchases
* and "subs" for subscriptions)
* @param developerPayload optional argument to be sent back with the purchase information
* @extraParams a Bundle with the following optional keys:
* "skusToReplace" - List<String> - an optional list of SKUs that the user is
* upgrading or downgrading from.
* Pass this field if the purchase is upgrading or downgrading
* existing subscriptions.
* The specified SKUs are replaced with the SKUs that the user is
* purchasing. Google Play replaces the specified SKUs at the start of
* the next billing cycle.
* "replaceSkusProration" - Boolean - whether the user should be credited for any unused
* subscription time on the SKUs they are upgrading or downgrading.
* If you set this field to true, Google Play swaps out the old SKUs
* and credits the user with the unused value of their subscription
* time on a pro-rated basis.
* Google Play applies this credit to the new subscription, and does
* not begin billing the user for the new subscription until after
* the credit is used up.
* If you set this field to false, the user does not receive credit for
* any unused subscription time and the recurrence date does not
* change.
* Default value is true. Ignored if you do not pass skusToReplace.
* "accountId" - String - an optional obfuscated string that is uniquely
* associated with the user's account in your app.
* If you pass this value, Google Play can use it to detect irregular
* activity, such as many devices making purchases on the same
* account in a short period of time.
* Do not use the developer ID or the user's Google ID for this field.
* In addition, this field should not contain the user's ID in
* cleartext.
* We recommend that you use a one-way hash to generate a string from
* the user's ID, and store the hashed string in this field.
* "vr" - Boolean - an optional flag indicating whether the returned intent
* should start a VR purchase flow. The apiVersion must also be 7 or
* later to use this flag.
*/
Bundle getBuyIntentExtraParams(int apiVersion, String packageName, String sku,
String type, String developerPayload, in Bundle extraParams) = 7;
/**
* Returns the most recent purchase made by the user for each SKU, even if that purchase is
* expired, canceled, or consumed.
* @param apiVersion billing API version that the app is using, must be 6 or later
* @param packageName package name of the calling app
* @param type of the in-app items being requested ("inapp" for one-time purchases
* and "subs" for subscriptions)
* @param continuationToken to be set as null for the first call, if the number of owned
* skus is too large, a continuationToken is returned in the response bundle.
* This method can be called again with the continuation token to get the next set of
* owned skus.
* @param extraParams a Bundle with the following optional keys:
* "playBillingLibraryVersion" - String
* "enablePendingPurchases" - Boolean
* @return Bundle containing the following key-value pairs
* "RESPONSE_CODE" with int value: RESULT_OK(0) if success,
* {@link IabHelper#BILLING_RESPONSE_RESULT_*} response codes on failures.
* "DEBUG_MESSAGE" - String
* "INAPP_PURCHASE_ITEM_LIST" - ArrayList<String> containing the list of SKUs
* "INAPP_PURCHASE_DATA_LIST" - ArrayList<String> containing the purchase information
* "INAPP_DATA_SIGNATURE_LIST"- ArrayList<String> containing the signatures
* of the purchase information
* "INAPP_CONTINUATION_TOKEN" - String containing a continuation token for the
* next set of in-app purchases. Only set if the
* user has more owned skus than the current list.
*/
Bundle getPurchaseHistory(int apiVersion, String packageName, String type,
String continuationToken, in Bundle extraParams) = 8;
/**
* This method is a variant of {@link #isBillingSupported}} that takes an additional
* {@code extraParams} parameter.
* @param apiVersion billing API version that the app is using, must be 7 or later
* @param packageName package name of the calling app
* @param type of the in-app item being purchased ("inapp" for one-time purchases and "subs"
* for subscriptions)
* @param extraParams a Bundle with the following optional keys:
* "vr" - Boolean - an optional flag to indicate whether {link #getBuyIntentExtraParams}
* supports returning a VR purchase flow.
* @return RESULT_OK(0) on success and appropriate response code on failures.
*/
int isBillingSupportedExtraParams(int apiVersion, String packageName, String type, in Bundle extraParams) = 9;
/**
* This method is a variant of {@link #getPurchases}} that takes an additional
* {@code extraParams} parameter.
* @param apiVersion billing API version that the app is using, must be 9 or later
* @param packageName package name of the calling app
* @param type of the in-app items being requested ("inapp" for one-time purchases
* and "subs" for subscriptions)
* @param continuationToken to be set as null for the first call, if the number of owned
* skus are too many, a continuationToken is returned in the response bundle.
* This method can be called again with the continuation token to get the next set of
* owned skus.
* @param extraParams a Bundle with the following optional keys:
* "playBillingLibraryVersion" - String
* "enablePendingPurchases" - Boolean
* @return Bundle containing the following key-value pairs
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response codes
on failures.
* "DEBUG_MESSAGE" - String
* "INAPP_PURCHASE_ITEM_LIST" - StringArrayList containing the list of SKUs
* "INAPP_PURCHASE_DATA_LIST" - StringArrayList containing the purchase information
* "INAPP_DATA_SIGNATURE_LIST"- StringArrayList containing the signatures
* of the purchase information
* "INAPP_CONTINUATION_TOKEN" - String containing a continuation token for the
* next set of in-app purchases. Only set if the
* user has more owned skus than the current list.
*/
Bundle getPurchasesExtraParams(int apiVersion, String packageName, String type, String continuationToken, in Bundle extraParams) = 10;
/**
* This method is a variant of {@link #consumePurchase}} that takes an additional
* {@code extraParams} parameter.
* @param apiVersion billing API version that the app is using, must be 9 or later
* @param packageName package name of the calling app
* @param purchaseToken token in the purchase information JSON that identifies the purchase
* to be consumed
* @param extraParams a Bundle with the following optional keys:
* "playBillingLibraryVersion" - String
* @return Bundle containing the following key-value pairs
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response codes
* on failures.
* "DEBUG_MESSAGE" - String
*/
Bundle consumePurchaseExtraParams(int apiVersion, String packageName, String purchaseToken, in Bundle extraParams) = 11;
Bundle getPriceChangeConfirmationIntent(int apiVersion, String packageName, String sku, String type, in Bundle extraParams) = 800;
/**
* This method is a variant of {@link #getSkuDetails}} that takes an additional
* {@code extraParams} parameter.
* @param apiVersion billing API version that the app is using, must be 9 or later
* @param packageName package name of the calling app
* @param type of the in-app item being purchased ("inapp" for one-time purchases and "subs"
* for subscriptions)
* @param skusBundle bundle containing a StringArrayList of SKUs with key "ITEM_ID_LIST"
* @param extraParams a Bundle with the following optional keys:
* "SKU_DETAILS_RESPONSE_FORMAT" - String
* "SKU_OFFER_ID_TOKEN_LIST" - ArrayList<String>
* "SKU_SERIALIZED_DOCID_LIST" - ArrayList<String>
* "accountName" - String
* "playBillingLibraryVersion" - String
* "enablePendingPurchases" - Boolean
* @return Bundle containing the following key-value pairs
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response codes
* on failures.
* "DEBUG_MESSAGE" - String
* "DETAILS_LIST" with a StringArrayList containing purchase information
* in JSON format similar to:
* '{ "productId" : "exampleSku",
* "type" : "inapp",
* "price" : "$5.00",
* "price_currency": "USD",
* "price_amount_micros": 5000000,
* "title : "Example Title",
* "description" : "This is an example description" }'
*/
Bundle getSkuDetailsExtraParams(int apiVersion, String packageName, String type, in Bundle skuBundle, in Bundle extraParams) = 900;
/**
* @param apiVersion billing API version that the app is using, must be 12 or later
* @param packageName package name of the calling app
* @param extraParams a Bundle with the following optional keys:
* "playBillingLibraryVersion" - String
* @return Bundle containing the following key-value pairs
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response codes
* on failures.
* "DEBUG_MESSAGE" - String
*/
Bundle acknowledgePurchase(int apiVersion, String packageName, String purchaseToken, in Bundle extraParams) = 901;
Bundle o(int apiVersion, String packageName, String arg3, in Bundle arg4) = 1100;
/**
* @param apiVersion billing API version that the app is using, must be 12 or later
* @param packageName package name of the calling app
* @param extraParams a Bundle with the following optional keys:
* "KEY_WINDOW_TOKEN" - IBinder
* "KEY_DIMEN_LEFT" - Integer
* "KEY_DIMEN_TOP" - Integer
* "KEY_DIMEN_RIGHT" - Integer
* "KEY_DIMEN_BOTTOM" - Integer
* "KEY_DIMEN_BOTTOM" - Integer
* "KEY_CATEGORY_IDS" - ArrayList<Integer>
* "playBillingLibraryVersion" - String
* @param callback callback that is invoked with the result, see IInAppBillingServiceCallback.aidl for details
*/
void showInAppMessages(int apiVersion, String packageName, in Bundle extraParams, IInAppBillingServiceCallback callback) = 1200;
/**
* @param apiVersion billing API version that the app is using, must be 18 or later
* @param packageName package name of the calling app
* @param extraParams a Bundle with the following optional keys:
* "playBillingLibraryVersion" - String
* @param callback callback that is invoked with the result, see IInAppBillingGetBillingConfigCallback.aidl for details
*/
void getBillingConfig(int apiVersion, String packageName, in Bundle extraParams, IInAppBillingGetBillingConfigCallback callback) = 1300;
/**
* @param apiVersion billing API version that the app is using, must be 21 or later
* @param packageName package name of the calling app
* @param extraParams a Bundle with the following optional keys:
* "playBillingLibraryVersion" - String
* @param callback callback that is invoked with the result, see IInAppBillingIsAlternativeBillingOnlyAvailableCallback.aidl for details
*/
void isAlternativeBillingOnlyAvailable(int apiVersion, String packageName, in Bundle extraParams, IInAppBillingIsAlternativeBillingOnlyAvailableCallback callback) = 1400;
/**
* @param apiVersion billing API version that the app is using, must be 21 or later
* @param packageName package name of the calling app
* @param extraParams a Bundle with the following optional keys:
* "playBillingLibraryVersion" - String
* @param callback callback that is invoked with the result, see IInAppBillingCreateAlternativeBillingOnlyTokenCallback.aidl for details
*/
void createAlternativeBillingOnlyToken(int apiVersion, String packageName, in Bundle extraParams, IInAppBillingCreateAlternativeBillingOnlyTokenCallback callback) = 1500;
/**
* @param apiVersion billing API version that the app is using, must be 21 or later
* @param packageName package name of the calling app
* @param extraParams a Bundle with the following optional keys:
* "playBillingLibraryVersion" - String
* @param callback callback that is invoked with the result, see IInAppBillingGetAlternativeBillingOnlyDialogIntentCallback.aidl for details
*/
void getAlternativeBillingOnlyDialogIntent(int apiVersion, String packageName, in Bundle extraParams, IInAppBillingGetAlternativeBillingOnlyDialogIntentCallback callback) = 1600;
/**
* @param apiVersion billing API version that the app is using, must be 22 or later
* @param packageName package name of the calling app
* @param extraParams a Bundle with the following optional keys:
* "playBillingLibraryVersion" - String
* @param callback callback that is invoked with the result, see IInAppBillingIsExternalPaymentAvailableCallback.aidl for details
*/
void isExternalOfferAvailable(int apiVersion, String packageName, in Bundle extraParams, IInAppBillingIsExternalPaymentAvailableCallback callback) = 1700;
/**
* @param apiVersion billing API version that the app is using, must be 22 or later
* @param packageName package name of the calling app
* @param extraParams a Bundle with the following optional keys:
* "playBillingLibraryVersion" - String
* @param callback callback that is invoked with the result, see IInAppBillingCreateExternalPaymentReportingDetailsCallback.aidl for details
*/
void createExternalOfferReportingDetails(int apiVersion, String packageName, in Bundle extraParams, IInAppBillingCreateExternalPaymentReportingDetailsCallback callback) = 1800;
/**
* @param apiVersion billing API version that the app is using, must be 22 or later
* @param packageName package name of the calling app
* @param extraParams a Bundle with the following optional keys:
* "playBillingLibraryVersion" - String
* @param callback callback that is invoked with the result, see IInAppBillingGetExternalPaymentDialogIntentCallback.aidl for details
*/
void showExternalOfferInformationDialog(int apiVersion, String packageName, in Bundle extraParams, IInAppBillingGetExternalPaymentDialogIntentCallback callback) = 1900;
void delegateToBackend(in Bundle bundle, IInAppBillingDelegateToBackendCallback callback) = 2000;
}

View file

@ -0,0 +1,15 @@
/*
* SPDX-FileCopyrightText: 2024 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.android.vending.billing;
import android.os.Bundle;
interface IInAppBillingServiceCallback {
/**
* @param bundle a Bundle with the following keys:
* "KEY_LAUNCH_INTENT" - PendingIntent
*/
void callback(in Bundle bundle);
}

View file

@ -0,0 +1,11 @@
/*
* SPDX-FileCopyrightText: 2010 The Android Open Source Project
* SPDX-FileCopyrightText: 2023 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.android.vending.licensing;
interface ILicenseResultListener {
oneway void verifyLicense(int responseCode, String signedData, String signature);
}

View file

@ -0,0 +1,11 @@
/*
* SPDX-FileCopyrightText: 2010 The Android Open Source Project
* SPDX-FileCopyrightText: 2023 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.android.vending.licensing;
interface ILicenseV2ResultListener {
oneway void verifyLicense(int responseCode, in Bundle responsePayload);
}

View file

@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: 2010 The Android Open Source Project
* SPDX-FileCopyrightText: 2023 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.android.vending.licensing;
import com.android.vending.licensing.ILicenseResultListener;
import com.android.vending.licensing.ILicenseV2ResultListener;
interface ILicensingService {
oneway void checkLicense(long nonce, String packageName, ILicenseResultListener listener);
oneway void checkLicenseV2(String packageName, ILicenseV2ResultListener listener, in Bundle extraParams);
}

View file

@ -0,0 +1,45 @@
/*
* SPDX-FileCopyrightText: 2025 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.engage.protocol;
import android.os.Bundle;
import com.google.android.engage.protocol.IAppEngageServicePublishClustersCallback;
import com.google.android.engage.protocol.IAppEngageServiceDeleteClustersCallback;
import com.google.android.engage.protocol.IAppEngageServiceAvailableCallback;
import com.google.android.engage.protocol.IAppEngageServicePublishStatusCallback;
interface IAppEngageService {
/**
* Publishes clusters of app engagement data.
*
* @param bundle Contains cluster data to be published
* @param callback Callback to receive results of the publish operation
*/
void publishClusters(in Bundle bundle, IAppEngageServicePublishClustersCallback callback);
/**
* Deletes previously published clusters of app engagement data.
*
* @param bundle Contains specifications about which clusters to delete
* @param callback Callback to receive results of the delete operation
*/
void deleteClusters(in Bundle bundle, IAppEngageServiceDeleteClustersCallback callback);
/**
* Checks if the App Engage Service is available for the calling application.
*
* @param bundle Contains parameters for the availability check
* @param callback Callback to receive availability status
*/
void isServiceAvailable(in Bundle bundle, IAppEngageServiceAvailableCallback callback);
/**
* Updates the publishing status for previously published clusters.
*
* @param bundle Contains status update information
* @param callback Callback to receive results of the status update operation
*/
void updatePublishStatus(in Bundle bundle, IAppEngageServicePublishStatusCallback callback);
}

View file

@ -0,0 +1,21 @@
/*
* SPDX-FileCopyrightText: 2025 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.engage.protocol;
import android.os.Bundle;
/**
* Callback interface for isServiceAvailable operation.
* This callback is used to receive the availability status of the App Engage Service.
*/
interface IAppEngageServiceAvailableCallback {
/**
* Called with the service availability result.
*
* @param result Bundle containing availability information.
* The key "availability" contains a boolean indicating whether the service is available.
*/
void onResult(in Bundle result);
}

View file

@ -0,0 +1,20 @@
/*
* SPDX-FileCopyrightText: 2025 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.engage.protocol;
import android.os.Bundle;
/**
* Callback interface for deleteClusters operation.
* This callback is used to receive the result of a cluster deletion operation.
*/
interface IAppEngageServiceDeleteClustersCallback {
/**
* Called when the delete operation has completed.
*
* @param result Bundle containing the result of the delete operation
*/
void onResult(in Bundle result);
}

View file

@ -0,0 +1,20 @@
/*
* SPDX-FileCopyrightText: 2025 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.engage.protocol;
import android.os.Bundle;
/**
* Callback interface for publishClusters operation.
* This callback is used to receive the result of a cluster publishing operation.
*/
interface IAppEngageServicePublishClustersCallback {
/**
* Called when the publish operation has completed.
*
* @param result Bundle containing the result of the publish operation
*/
void onResult(in Bundle result);
}

View file

@ -0,0 +1,20 @@
/*
* SPDX-FileCopyrightText: 2025 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.engage.protocol;
import android.os.Bundle;
/**
* Callback interface for updatePublishStatus operation.
* This callback is used to receive the result of a status update operation.
*/
interface IAppEngageServicePublishStatusCallback {
/**
* Called when the status update operation has completed.
*
* @param result Bundle containing the result of the status update operation
*/
void onResult(in Bundle result);
}

View file

@ -0,0 +1,10 @@
/*
* SPDX-FileCopyrightText: 2023 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.finsky.externalreferrer;
interface IGetInstallReferrerService {
Bundle getInstallReferrer(in Bundle request);
}

View file

@ -0,0 +1,10 @@
/*
* SPDX-FileCopyrightText: 2023 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.finsky.services;
interface IPlayGearheadService {
Bundle isPackageInstalledByPlayCheck(in String pkgName);
}

View file

@ -0,0 +1,7 @@
package com.google.android.gms.checkin.internal;
interface ICheckinService {
String getDeviceDataVersionInfo();
long getLastCheckinSuccessTime();
String getLastSimOperator();
}

View file

@ -0,0 +1,14 @@
/*
* SPDX-FileCopyrightText: 2023 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.play.core.appupdate.protocol;
import com.google.android.play.core.appupdate.protocol.IAppUpdateServiceCallback;
interface IAppUpdateService {
oneway void requestUpdateInfo(String packageName, in Bundle bundle, in IAppUpdateServiceCallback callback) = 1;
oneway void completeUpdate(String packageName, in Bundle bundle, in IAppUpdateServiceCallback callback) = 2;
oneway void updateProgress(in Bundle bundle) = 3;
}

View file

@ -0,0 +1,11 @@
/*
* SPDX-FileCopyrightText: 2023 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.play.core.appupdate.protocol;
interface IAppUpdateServiceCallback {
oneway void onUpdateResult(in Bundle bundle) = 1;
oneway void onCompleteResult(in Bundle bundle) = 2;
}

View file

@ -0,0 +1,21 @@
/*
* SPDX-FileCopyrightText: 2023 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.play.core.assetpacks.protocol;
import com.google.android.play.core.assetpacks.protocol.IAssetModuleServiceCallback;
interface IAssetModuleService {
oneway void startDownload(String packageName, in List<Bundle> list, in Bundle bundle, in IAssetModuleServiceCallback callback) = 1;
oneway void getSessionStates(String packageName, in Bundle bundle, in IAssetModuleServiceCallback callback) = 4;
oneway void notifyChunkTransferred(String packageName, in Bundle bundle, in Bundle bundle2, in IAssetModuleServiceCallback callback) = 5;
oneway void notifyModuleCompleted(String packageName, in Bundle bundle, in Bundle bundle2, in IAssetModuleServiceCallback callback) = 6;
oneway void notifySessionFailed(String packageName, in Bundle bundle, in Bundle bundle2, in IAssetModuleServiceCallback callback) = 8;
oneway void keepAlive(String packageName, in Bundle bundle, in IAssetModuleServiceCallback callback) = 9;
oneway void getChunkFileDescriptor(String packageName, in Bundle bundle, in Bundle bundle2, in IAssetModuleServiceCallback callback) = 10;
oneway void requestDownloadInfo(String packageName, in List<Bundle> list, in Bundle bundle, in IAssetModuleServiceCallback callback) = 11;
oneway void removeModule(String packageName, in Bundle bundle, in Bundle bundle2, in IAssetModuleServiceCallback callback) = 12;
oneway void cancelDownloads(String packageName, in List<Bundle> list, in Bundle bundle, in IAssetModuleServiceCallback callback) = 13;
}

View file

@ -0,0 +1,22 @@
/*
* SPDX-FileCopyrightText: 2023 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.play.core.assetpacks.protocol;
interface IAssetModuleServiceCallback {
oneway void onStartDownload(int sessionId, in Bundle bundle) = 1;
oneway void onCancelDownload(int status, in Bundle bundle) = 2;
oneway void onGetSession(int status, in Bundle bundle) = 3;
oneway void onGetSessionStates(in List<Bundle> list) = 4;
oneway void onNotifyChunkTransferred(in Bundle bundle, in Bundle bundle2) = 5;
oneway void onError(in Bundle bundle) = 6;
oneway void onNotifyModuleCompleted(in Bundle bundle, in Bundle bundle2) = 7;
oneway void onNotifySessionFailed(in Bundle bundle) = 9;
oneway void onKeepAlive(in Bundle bundle, in Bundle bundle2) = 10;
oneway void onGetChunkFileDescriptor(in Bundle bundle, in Bundle bundle2) = 11;
oneway void onRequestDownloadInfo(in Bundle bundle, in Bundle bundle2) = 12;
oneway void onRemoveModule(in Bundle bundle, in Bundle bundle2) = 13;
oneway void onCancelDownloads(in Bundle bundle) = 14;
}

View file

@ -0,0 +1,12 @@
/*
* SPDX-FileCopyrightText: 2025 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.play.core.inappreview.protocol;
import com.google.android.play.core.inappreview.protocol.IInAppReviewServiceCallback;
interface IInAppReviewService {
oneway void requestInAppReview(String packageName, in Bundle bundle, in IInAppReviewServiceCallback callback) = 1;
}

View file

@ -0,0 +1,12 @@
/*
* SPDX-FileCopyrightText: 2025 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.play.core.inappreview.protocol;
import android.os.Bundle;
interface IInAppReviewServiceCallback {
void onResult(in Bundle bundle);
}

View file

@ -0,0 +1,15 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.play.core.integrity.protocol;
import com.google.android.play.core.integrity.protocol.IExpressIntegrityServiceCallback;
import com.google.android.play.core.integrity.protocol.IRequestDialogCallback;
interface IExpressIntegrityService {
void warmUpIntegrityToken(in Bundle bundle, in IExpressIntegrityServiceCallback callback) = 1;
void requestExpressIntegrityToken(in Bundle bundle, in IExpressIntegrityServiceCallback callback) = 2;
void requestAndShowDialog(in Bundle bundle, in IRequestDialogCallback callback) = 5;
}

View file

@ -0,0 +1,12 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.play.core.integrity.protocol;
interface IExpressIntegrityServiceCallback {
void onWarmUpExpressIntegrityToken(in Bundle bundle) = 1;
void onRequestExpressIntegrityToken(in Bundle bundle) = 2;
void onRequestIntegrityToken(in Bundle bundle) = 3;
}

View file

@ -0,0 +1,15 @@
/*
* SPDX-FileCopyrightText: 2023 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.play.core.integrity.protocol;
import com.google.android.play.core.integrity.protocol.IIntegrityServiceCallback;
import com.google.android.play.core.integrity.protocol.IRequestDialogCallback;
interface IIntegrityService {
void requestDialog(in Bundle bundle, in IRequestDialogCallback callback) = 0;
void requestIntegrityToken(in Bundle request, in IIntegrityServiceCallback callback) = 1;
void requestAndShowDialog(in Bundle bundle, in IRequestDialogCallback callback) = 2;
}

View file

@ -0,0 +1,10 @@
/*
* SPDX-FileCopyrightText: 2023 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.play.core.integrity.protocol;
interface IIntegrityServiceCallback {
void onRequestIntegrityToken(in Bundle bundle) = 1;
}

View file

@ -0,0 +1,10 @@
/*
* SPDX-FileCopyrightText: 2023 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.play.core.integrity.protocol;
interface IRequestDialogCallback {
void onRequestDialog(in Bundle bundle);
}

View file

@ -0,0 +1,22 @@
/**
* SPDX-FileCopyrightText: 2024 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.play.core.splitinstall.protocol;
import com.google.android.play.core.splitinstall.protocol.ISplitInstallServiceCallback;
interface ISplitInstallService {
void startInstall(String pkg,in List<Bundle> splits,in Bundle bundle, ISplitInstallServiceCallback callback) = 1;
void completeInstalls(String pkg, int sessionId,in Bundle bundle, ISplitInstallServiceCallback callback) = 2;
void cancelInstall(String pkg, int sessionId, ISplitInstallServiceCallback callback) = 3;
void getSessionState(String pkg, int sessionId, ISplitInstallServiceCallback callback) = 4;
void getSessionStates(String pkg, ISplitInstallServiceCallback callback) = 5;
void splitRemoval(String pkg,in List<Bundle> splits, ISplitInstallServiceCallback callback) = 6;
void splitDeferred(String pkg,in List<Bundle> splits,in Bundle bundle, ISplitInstallServiceCallback callback) = 7;
void getSessionState2(String pkg, int sessionId, ISplitInstallServiceCallback callback) = 8;
void getSessionStates2(String pkg, ISplitInstallServiceCallback callback) = 9;
void getSplitsAppUpdate(String pkg, ISplitInstallServiceCallback callback) = 10;
void completeInstallAppUpdate(String pkg, ISplitInstallServiceCallback callback) = 11;
void languageSplitInstall(String pkg,in List<Bundle> splits,in Bundle bundle, ISplitInstallServiceCallback callback) = 12;
void languageSplitUninstall(String pkg,in List<Bundle> splits, ISplitInstallServiceCallback callback) =13;
}

View file

@ -0,0 +1,19 @@
/**
* SPDX-FileCopyrightText: 2024 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.play.core.splitinstall.protocol;
interface ISplitInstallServiceCallback {
oneway void onStartInstall(int status, in Bundle bundle) = 1;
oneway void onInstallCompleted(int status, in Bundle bundle) = 2;
oneway void onCancelInstall(int status, in Bundle bundle) = 3;
oneway void onGetSessionState(int status, in Bundle bundle) = 4;
oneway void onError(in Bundle bundle) = 5;
oneway void onGetSessionStates(in List<Bundle> list) = 6;
oneway void onDeferredUninstall(in Bundle bundle) = 7;
oneway void onDeferredInstall(in Bundle bundle) = 8;
oneway void onDeferredLanguageInstall(in Bundle bundle) = 11;
oneway void onDeferredLanguageUninstall(in Bundle bundle) = 12;
}

View file

@ -0,0 +1,217 @@
/*
* SPDX-FileCopyrightText: 2024 microG Project Team
* SPDX-License-Identifier: Apache-2.0
* Notice: Portions of this file are reproduced from work created and shared by Google and used
* according to terms described in the Creative Commons 4.0 Attribution License.
* See https://developers.google.com/readme/policies for details.
*/
package com.android.billingclient.api;
import android.app.Activity;
import android.os.Bundle;
import androidx.annotation.AnyThread;
import androidx.annotation.UiThread;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Main interface for communication between the library and user application code.
* <p>
* It provides convenience methods for in-app billing. You can create one instance of this class for your application and
* use it to process in-app billing operations. It provides synchronous (blocking) and asynchronous (non-blocking)
* methods for many common in-app billing operations.
* <p>
* It's strongly recommended that you instantiate only one BillingClient instance at one time to avoid multiple
* {@link PurchasesUpdatedListener#onPurchasesUpdated} callbacks for a single event.
* <p>
* All methods annotated with {@link AnyThread} can be called from any thread and all the asynchronous callbacks will be
* returned on the same thread. Methods annotated with {@link UiThread} should be called from the Ui thread and all the
* asynchronous callbacks will be returned on the Ui thread as well.
* <p>
* After instantiating, you must perform setup in order to start using the object. To perform setup, call the
* {@link #startConnection} method and provide a listener; that listener will be notified when setup is complete, after which
* (and not before) you may start calling other methods. After setup is complete, you will typically want to request an
* inventory of owned items and subscriptions. See {@link #queryPurchasesAsync} and {@link #queryProductDetailsAsync}.
* <p>
* When you are done with this object, don't forget to call {@link #endConnection} to ensure proper cleanup. This object holds a
* binding to the in-app billing service and the manager to handle broadcast events, which will leak unless you dispose it
* correctly. If you created the object inside the {@link Activity#onCreate(Bundle)} method, then the recommended place to dispose is
* the {@link Activity#onDestroy()} method. After cleanup, it cannot be reused again for connection.
* <p>
* To get library logs inside Android logcat, set corresponding logging level. E.g.: {@code adb shell setprop
* log.tag.BillingClient VERBOSE}
*/
public abstract class BillingClient {
/**
* Possible response codes.
*/
@Retention(RetentionPolicy.SOURCE)
public @interface BillingResponseCode {
/**
* The request has reached the maximum timeout before Google Play responds.
* <p>
* Since this state is transient, your app should automatically retry (e.g. with exponential back off) to recover from this
* error. Be mindful of how long you retry if the retry is happening during a user interaction.
*
* @deprecated See {@link #SERVICE_UNAVAILABLE} which will be used instead of this code.
*/
@Deprecated
int SERVICE_TIMEOUT = -3;
/**
* The requested feature is not supported by the Play Store on the current device.
* <p>
* If your app would like to check if a feature is supported before trying to use the feature your app can call
* {@link #isFeatureSupported} to check if a feature is supported. For a list of feature types that can be supported, see
* {@link FeatureType}.
* <p>
* For example: Before calling {@link #showInAppMessages} API, you can call {@link #isFeatureSupported} with the
* {@link FeatureType#IN_APP_MESSAGING} featureType to check if it is supported.
*/
int FEATURE_NOT_SUPPORTED = -2;
/**
* The app is not connected to the Play Store service via the Google Play Billing Library.
* <p>
* Examples where this error may occur:
* <ul>
* <li>The Play Store could have been updated in the background while your app was still running and the library lost
* connection.</li>
* <li>{@link #startConnection} was never called or has not completed yet.</li>
* </ul>
* Since this state is transient, your app should automatically retry (e.g. with exponential back off) to recover from this
* error. Be mindful of how long you retry if the retry is happening during a user interaction. The retry should lead to a
* call to {@link #startConnection} right after or in some time after you received this code.
*/
int SERVICE_DISCONNECTED = -1;
/**
* Success.
*/
int OK = 0;
/**
* Transaction was canceled by the user.
*/
int USER_CANCELED = 1;
/**
* The service is currently unavailable.
* <p>
* Since this state is transient, your app should automatically retry (e.g. with exponential back off) to recover from this
* error. Be mindful of how long you retry if the retry is happening during a user interaction.
*/
int SERVICE_UNAVAILABLE = 2;
/**
* A user billing error occurred during processing.
* <p>
* Examples where this error may occur:
* <ul>
* <li>The Play Store app on the user's device is out of date.</li>
* <li>The user is in an unsupported country.</li>
* <li>The user is an enterprise user and their enterprise admin has disabled users from making purchases.</li>
* <li>Google Play is unable to charge the user?s payment method.</li>
* </ul>
* Letting the user retry may succeed if the condition causing the error has changed (e.g. An enterprise user's admin has allowed purchases for the organization).
*/
int BILLING_UNAVAILABLE = 3;
/**
* The requested product is not available for purchase.
* <p>
* Please ensure the product is available in the user?s country. If you recently changed the country availability and are
* still receiving this error then it may be because of a propagation delay.
*/
int ITEM_UNAVAILABLE = 4;
/**
* Error resulting from incorrect usage of the API.
* <p>
* Examples where this error may occur:
* <ul>
* <li>Invalid arguments such as providing an empty product list where required.</li>
* <li>Misconfiguration of the app such as not signing the app or not having the necessary permissions in the manifest.</li>
* </ul>
*/
int DEVELOPER_ERROR = 5;
/**
* Fatal error during the API action.
* <p>
* This is an internal Google Play error that may be transient or due to an unexpected condition during processing. You
* can automatically retry (e.g. with exponential back off) for this case and contact Google Play if issues persist. Be
* mindful of how long you retry if the retry is happening during a user interaction.
*/
int ERROR = 6;
/**
* The purchase failed because the item is already owned.
* <p>
* Make sure your app is up-to-date with recent purchases using guidance in the Fetching purchases section in the
* integration guide. If this error occurs despite making the check for recent purchases, then it may be due to stale
* purchase information that was cached on the device by Play. When you receive this error, the cache should get
* updated. After this, your purchases should be reconciled, and you can process them as outlined in the processing
* purchases section in the integration guide.
*/
int ITEM_ALREADY_OWNED = 7;
/**
* Requested action on the item failed since it is not owned by the user.
* <p>
* Make sure your app is up-to-date with recent purchases using guidance in the Fetching purchases section in the
* integration guide. If this error occurs despite making the check for recent purchases, then it may be due to stale
* purchase information cached on the device by Play. When you receive this error, the cache should get updated. After
* this, your purchases should be reconciled, and you can process the purchases accordingly. For example, if you are
* trying to consume an item and if the updated purchase information says it is already consumed, you can ignore the
* error now.
*/
int ITEM_NOT_OWNED = 8;
int EXPIRED_OFFER_TOKEN = 11;
/**
* A network error occurred during the operation.
* <p>
* This error indicates that there was a problem with the network connection between the device and Play systems. This
* could potentially also be due to the user not having an active network connection.
*/
int NETWORK_ERROR = 12;
int RESPONSE_CODE_UNSPECIFIED = -999;
}
/**
* Supported Product types.
*/
@Retention(RetentionPolicy.SOURCE)
public @interface ProductType {
/**
* A Product type for Android apps in-app products.
*/
String INAPP = "inapp";
/**
* A Product type for Android apps subscriptions.
*/
String SUBS = "subs";
}
/**
* Supported SKU types.
* @deprecated Use {@link ProductType} instead.
*/
@Deprecated
@Retention(RetentionPolicy.SOURCE)
public @interface SkuType {
/**
* A type of SKU for Android apps in-app products.
*/
String INAPP = ProductType.INAPP;
/**
* A type of SKU for Android apps subscriptions.
*/
String SUBS = ProductType.SUBS;
}
}

View file

@ -0,0 +1,9 @@
/*
* SPDX-FileCopyrightText: 2024 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
/**
* TODO: Move to own library, as a drop-in to replace com.android.billingclient:billing
*/
package com.android.billingclient;

View file

@ -0,0 +1,33 @@
/*
* SPDX-FileCopyrightText: 2016 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.android.vending;
import android.app.Activity;
import android.content.pm.PackageManager;
import android.os.Bundle;
import androidx.annotation.RequiresApi;
@RequiresApi(23)
public class GrantFakeSignaturePermissionActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (checkSelfPermission("android.permission.FAKE_PACKAGE_SIGNATURE") != PackageManager.PERMISSION_GRANTED) {
requestPermissions(new String[]{"android.permission.FAKE_PACKAGE_SIGNATURE"}, 1);
} else {
setResult(RESULT_OK);
finish();
}
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
if (requestCode == 1 && grantResults.length == 1) {
setResult(grantResults[0] == PackageManager.PERMISSION_GRANTED ? RESULT_OK : RESULT_CANCELED);
finish();
}
}
}

View file

@ -0,0 +1,83 @@
/*
* SPDX-FileCopyrightText: 2023, e Foundation
* SPDX-FileCopyrightText: 2024 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.android.vending
import android.content.Context
import org.microg.gms.settings.SettingsContract
object VendingPreferences {
@JvmStatic
fun isLicensingEnabled(context: Context): Boolean {
val projection = arrayOf(SettingsContract.Vending.LICENSING)
return SettingsContract.getSettings(context, SettingsContract.Vending.getContentUri(context), projection) { c ->
c.getInt(0) != 0
}
}
@JvmStatic
fun isLicensingPurchaseFreeAppsEnabled(context: Context): Boolean {
val projection = arrayOf(SettingsContract.Vending.LICENSING_PURCHASE_FREE_APPS)
return SettingsContract.getSettings(context, SettingsContract.Vending.getContentUri(context), projection) { c ->
c.getInt(0) != 0
}
}
@JvmStatic
fun isSplitInstallEnabled(context: Context): Boolean {
val projection = arrayOf(SettingsContract.Vending.SPLIT_INSTALL)
return SettingsContract.getSettings(context, SettingsContract.Vending.getContentUri(context), projection) { c ->
c.getInt(0) != 0
}
}
@JvmStatic
fun isBillingEnabled(context: Context): Boolean {
val projection = arrayOf(SettingsContract.Vending.BILLING)
return SettingsContract.getSettings(context, SettingsContract.Vending.getContentUri(context), projection) { c ->
c.getInt(0) != 0
}
}
@JvmStatic
fun isAssetDeliveryEnabled(context: Context): Boolean {
val projection = arrayOf(SettingsContract.Vending.ASSET_DELIVERY)
return SettingsContract.getSettings(context, SettingsContract.Vending.getContentUri(context), projection) { c ->
c.getInt(0) != 0
}
}
@JvmStatic
fun isDeviceSyncEnabled(context: Context): Boolean {
val projection = arrayOf(SettingsContract.Vending.ASSET_DEVICE_SYNC)
return SettingsContract.getSettings(context, SettingsContract.Vending.getContentUri(context), projection) { c ->
c.getInt(0) != 0
}
}
@JvmStatic
fun isInstallEnabled(context: Context): Boolean {
val projection = arrayOf(SettingsContract.Vending.APPS_INSTALL)
return SettingsContract.getSettings(context, SettingsContract.Vending.getContentUri(context), projection) { c ->
c.getInt(0) != 0
}
}
@JvmStatic
fun getInstallerList(context: Context): String {
val projection = arrayOf(SettingsContract.Vending.APPS_INSTALLER_LIST)
return SettingsContract.getSettings(context, SettingsContract.Vending.getContentUri(context), projection) { c ->
c.getString(0)
}
}
@JvmStatic
fun setInstallerList(context: Context, content: String) {
SettingsContract.setSettings(context, SettingsContract.Vending.getContentUri(context)) {
put(SettingsContract.Vending.APPS_INSTALLER_LIST, content)
}
}
}

View file

@ -0,0 +1,24 @@
package com.android.vending.billing
import android.app.Service
import android.content.Intent
import android.os.IBinder
import androidx.annotation.RequiresApi
import org.microg.gms.profile.ProfileManager
import org.microg.vending.billing.ContextProvider
import org.microg.vending.billing.InAppBillingServiceImpl
@RequiresApi(21)
class InAppBillingService: Service() {
override fun onCreate() {
super.onCreate()
ProfileManager.ensureInitialized(this)
ContextProvider.init(application)
}
override fun onBind(intent: Intent): IBinder {
return InAppBillingServiceImpl(this)
}
}

View file

@ -0,0 +1,161 @@
package com.android.vending.licensing
import android.accounts.Account
import android.accounts.AccountManager
import android.accounts.AuthenticatorException
import android.accounts.OperationCanceledException
import android.content.pm.PackageInfo
import android.os.RemoteException
import android.util.Log
import com.android.vending.AUTH_TOKEN_SCOPE
import com.android.vending.buildRequestHeaders
import com.android.vending.getAuthToken
import org.microg.vending.billing.core.HttpClient
import org.microg.vending.billing.proto.GoogleApiResponse
import java.io.IOException
private const val TAG = "FakeLicenseChecker"
/* Possible response codes for checkLicense v1, from
* https://developer.android.com/google/play/licensing/licensing-reference#server-response-codes and
* the LVL library.
*/
/**
* The application is licensed to the user. The user has purchased the application, or is authorized to
* download and install the alpha or beta version of the application.
*/
const val LICENSED: Int = 0x0
/**
* The application is not licensed to the user.
*/
const val NOT_LICENSED: Int = 0x1
/**
* The application is licensed to the user, but there is an updated application version available that is
* signed with a different key.
*/
const val LICENSED_OLD_KEY: Int = 0x2
/**
* Server error the application (package name) was not recognized by Google Play.
*/
const val ERROR_NOT_MARKET_MANAGED: Int = 0x3
/**
* Server error the server could not load the application's key pair for licensing.
*/
const val ERROR_SERVER_FAILURE: Int = 0x4
const val ERROR_OVER_QUOTA: Int = 0x5
/**
* Local error the Google Play application was not able to reach the licensing server, possibly because
* of network availability problems.
*/
const val ERROR_CONTACTING_SERVER: Int = 0x101
/**
* Local error the application requested a license check for a package that is not installed on the device.
*/
const val ERROR_INVALID_PACKAGE_NAME: Int = 0x102
/**
* Local error the application requested a license check for a package whose UID (package, user ID pair)
* does not match that of the requesting application.
*/
const val ERROR_NON_MATCHING_UID: Int = 0x103
sealed class LicenseRequestParameters
data class V1Parameters(
val nonce: Long
) : LicenseRequestParameters()
object V2Parameters : LicenseRequestParameters()
sealed class LicenseResponse(
val result: Int
)
class V1Response(
result: Int,
val signedData: String,
val signature: String
) : LicenseResponse(result)
class V2Response(
result: Int,
val jwt: String?
): LicenseResponse(result)
class ErrorResponse(
result: Int
): LicenseResponse(result)
/**
* Performs license check including caller UID verification, using a given account, for which
* an auth token is fetched.
*/
@Throws(RemoteException::class)
suspend fun HttpClient.checkLicense(
account: Account,
accountManager: AccountManager,
androidId: String?,
packageInfo: PackageInfo,
packageName: String,
queryData: LicenseRequestParameters
) : LicenseResponse {
val auth = try {
getAuthToken(accountManager, account, AUTH_TOKEN_SCOPE)
.getString(AccountManager.KEY_AUTHTOKEN)
} catch (e: AuthenticatorException) {
Log.e(TAG, "Could not fetch auth token for account $account")
return ErrorResponse(ERROR_CONTACTING_SERVER)
}
if (auth == null) {
return ErrorResponse(ERROR_CONTACTING_SERVER)
}
val decodedAndroidId = androidId?.toLong(16) ?: 1
return try {
when (queryData) {
is V1Parameters -> makeLicenseV1Request(
packageName, auth, packageInfo.versionCode, queryData.nonce, decodedAndroidId
)
is V2Parameters -> makeLicenseV2Request(
packageName, auth, packageInfo.versionCode, decodedAndroidId
)
} ?: ErrorResponse(NOT_LICENSED)
} catch (e: IOException) {
Log.e(TAG, "Encountered a network error during operation", e)
ErrorResponse(ERROR_CONTACTING_SERVER)
} catch (e: OperationCanceledException) {
ErrorResponse(ERROR_CONTACTING_SERVER)
}
}
suspend fun HttpClient.makeLicenseV1Request(
packageName: String, auth: String, versionCode: Int, nonce: Long, androidId: Long
): V1Response? = get(
url = "https://play-fe.googleapis.com/fdfe/apps/checkLicense?pkgn=$packageName&vc=$versionCode&nnc=$nonce",
headers = buildRequestHeaders(auth, androidId),
adapter = GoogleApiResponse.ADAPTER
).payload?.licenseV1Response?.let {
if (it.result != null && it.signedData != null && it.signature != null) {
V1Response(it.result, it.signedData, it.signature)
} else null
}
suspend fun HttpClient.makeLicenseV2Request(
packageName: String,
auth: String,
versionCode: Int,
androidId: Long
): V2Response? = get(
url = "https://play-fe.googleapis.com/fdfe/apps/checkLicenseServerFallback?pkgn=$packageName&vc=$versionCode",
headers = buildRequestHeaders(auth, androidId),
adapter = GoogleApiResponse.ADAPTER
).payload?.licenseV2Response?.license?.jwt?.let {
// Field present ←→ user has license
V2Response(LICENSED, it)
}

View file

@ -0,0 +1,159 @@
package com.android.vending.licensing
import android.Manifest
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build.VERSION.SDK_INT
import android.util.Log
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.PendingIntentCompat
import com.android.vending.R
import java.util.TreeSet
private const val TAG = "FakeLicenseNotification"
private const val GMS_PACKAGE_NAME = "com.google.android.gms"
private const val GMS_AUTH_INTENT_ACTION = "com.google.android.gms.auth.login.LOGIN"
private const val PREFERENCES_KEY_IGNORE_PACKAGES_LIST = "ignorePackages"
private const val PREFERENCES_FILE_NAME = "licensing"
private const val INTENT_KEY_IGNORE_PACKAGE_NAME = "package"
private const val INTENT_KEY_NOTIFICATION_ID = "id"
private const val CHANNEL_ID = "LicenseNotification"
fun Context.sendLicenseServiceNotification(
callerPackageName: String,
callerAppName: CharSequence,
callerUid: Int
) {
registerLicenseServiceNotificationChannel()
val preferences = getSharedPreferences(PREFERENCES_FILE_NAME, Context.MODE_PRIVATE)
val ignoreList = preferences.getStringSet(PREFERENCES_KEY_IGNORE_PACKAGES_LIST, emptySet())
for (ignoredPackage in ignoreList!!) {
if (callerPackageName == ignoredPackage) {
Log.d(TAG, "Not notifying about license check, as user has ignored " +
"notifications for package $ignoredPackage"
)
return
}
}
val authIntent = Intent(this, SignInReceiver::class.java).apply {
putExtra(INTENT_KEY_NOTIFICATION_ID, callerUid)
}.let {
PendingIntentCompat.getBroadcast(
this, callerUid * 2, it, 0, false
)
}
val ignoreIntent = Intent(this, IgnoreReceiver::class.java).apply {
putExtra(INTENT_KEY_IGNORE_PACKAGE_NAME, callerPackageName)
putExtra(INTENT_KEY_NOTIFICATION_ID, callerUid)
}.let {
PendingIntentCompat.getBroadcast(
this, callerUid * 2 + 1, it, 0, true
)
}
val contentText = getString(R.string.license_notification_body)
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification)
.setSound(null)
.setContentTitle(getString(R.string.license_notification_title, callerAppName))
.setContentText(contentText)
.setStyle(NotificationCompat.BigTextStyle().bigText(contentText))
.addAction(
NotificationCompat.Action.Builder(
null,
getString(R.string.license_notification_sign_in),
authIntent
).build()
)
.addAction(
NotificationCompat.Action.Builder(
null,
getString(R.string.license_notification_ignore),
ignoreIntent
).build()
)
.setAutoCancel(true)
.build()
val notificationManager = NotificationManagerCompat.from(this)
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) ==
PackageManager.PERMISSION_GRANTED
) {
notificationManager.notify(callerUid, notification)
}
}
private fun Context.registerLicenseServiceNotificationChannel() {
if (SDK_INT >= 26) {
val channel = NotificationChannel(
CHANNEL_ID,
getString(R.string.license_notification_channel_name),
NotificationManager.IMPORTANCE_HIGH
)
channel.description =
getString(R.string.license_notification_channel_description)
channel.setSound(null, null)
val notificationManager = getSystemService(
NotificationManager::class.java
)
notificationManager.createNotificationChannel(channel)
}
}
class IgnoreReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
// Dismiss ignored notification
NotificationManagerCompat.from(context)
.cancel(intent.getIntExtra(INTENT_KEY_NOTIFICATION_ID, -1))
val preferences =
context.getSharedPreferences(PREFERENCES_FILE_NAME, Context.MODE_PRIVATE)
val ignoreList: MutableSet<String> = TreeSet(
preferences.getStringSet(PREFERENCES_KEY_IGNORE_PACKAGES_LIST, emptySet())
)
val newIgnorePackage = intent.getStringExtra(INTENT_KEY_IGNORE_PACKAGE_NAME)
if (newIgnorePackage == null) {
Log.e(TAG, "Received no ignore package; can't add to ignore list.")
return
}
Log.d(TAG, "Adding package $newIgnorePackage to ignore list")
ignoreList.add(newIgnorePackage)
preferences.edit()
.putStringSet(PREFERENCES_KEY_IGNORE_PACKAGES_LIST, ignoreList)
.apply()
}
}
class SignInReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
// Dismiss all notifications
NotificationManagerCompat.from(context).cancelAll()
Log.d(TAG, "Starting sign in activity")
Intent(GMS_AUTH_INTENT_ACTION).apply {
setPackage(GMS_PACKAGE_NAME)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}.let { context.startActivity(it) }
}
}

View file

@ -0,0 +1,215 @@
/*
* SPDX-FileCopyrightText: 2023 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.android.vending.licensing
import android.accounts.Account
import android.accounts.AccountManager
import android.app.Service
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Bundle
import android.os.IBinder
import android.os.RemoteException
import android.util.Log
import com.android.vending.VendingPreferences.isLicensingEnabled
import com.android.vending.VendingPreferences.isLicensingPurchaseFreeAppsEnabled
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() {
private lateinit var accountManager: AccountManager
private lateinit var androidId: String
private lateinit var httpClient: HttpClient
private val mLicenseService: ILicensingService.Stub = object : ILicensingService.Stub() {
@Throws(RemoteException::class)
override fun checkLicense(
nonce: Long,
packageName: String,
listener: ILicenseResultListener
): Unit = runBlocking {
Log.v(TAG, "checkLicense($nonce, $packageName)")
val response = checkLicenseCommon(packageName, V1Parameters(nonce))
/* If a license is found, it is now stored in `lastResponse`. Otherwise, it now contains
* an error. In either case, we should send it to the application.
*/
try {
when (response) {
is V1Response -> listener.verifyLicense(response.result, response.signedData, response.signature)
is ErrorResponse -> listener.verifyLicense(response.result, null, null)
is V2Response -> Unit // should never happen
null -> Unit // no license check was performed at all
}
} catch (e: Exception) {
Log.w(TAG, "Remote threw an exception while returning license result ${response}")
}
}
@Throws(RemoteException::class)
override fun checkLicenseV2(
packageName: String,
listener: ILicenseV2ResultListener,
extraParams: Bundle
): Unit = runBlocking {
Log.v(TAG, "checkLicenseV2($packageName, $extraParams)")
val response = checkLicenseCommon(packageName, V2Parameters)
/*
* Suppress failures on V2. V2 is commonly used by free apps whose checker
* will not throw users out of the app if it never receives a response.
*
* This means that users who are signed in to a Google account will not
* get a worse experience in these apps than users that are not signed in.
*
* Normally, we would otherwise always send the response.
*/
if (response?.result == LICENSED && response is V2Response) {
val bundle = Bundle()
bundle.putString(KEY_V2_RESULT_JWT, response.jwt)
try {
listener.verifyLicense(response.result, bundle)
} 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")
}
}
/**
* Checks for license on all accounts.
*
* @return `null` if no check is performed (for example, because the feature is disabled),
* an instance of [LicenseResponse] otherwise.
*/
suspend fun checkLicenseCommon(
packageName: String,
request: LicenseRequestParameters
): LicenseResponse? {
val callingUid = getCallingUid()
if (!isLicensingEnabled(this@LicensingService)) {
Log.d(TAG, "not checking license, as it is disabled by user")
return null
}
val packageInfo = try {
packageManager.getPackageInfo(packageName, 0)
} catch (e: PackageManager.NameNotFoundException) {
Log.e(TAG,
"an app tried to request licenses for package $packageName, which does not exist"
)
return ErrorResponse(ERROR_INVALID_PACKAGE_NAME)
}
// Verify caller identity
if (packageInfo.applicationInfo?.uid != callingUid) {
Log.e(
TAG,
"an app illegally tried to request licenses for another app (caller: $callingUid)"
)
return ErrorResponse(ERROR_NON_MATCHING_UID)
}
val accounts = accountManager.getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE)
val packageManager = packageManager
lateinit var lastResponse: LicenseResponse
if (accounts.isEmpty()) {
handleNoAccounts(packageName, packageManager)
return null
} else for (account: Account in accounts) {
lastResponse = httpClient.checkLicense(
account, accountManager, androidId, packageInfo, packageName, request
)
if (lastResponse.result == LICENSED) {
return lastResponse;
}
}
// Attempt to acquire license if app is free ("auto-purchase")
if (isLicensingPurchaseFreeAppsEnabled(this@LicensingService)) {
val firstAccount = accounts[0]
if (httpClient.acquireFreeAppLicense(
this@LicensingService,
firstAccount,
packageName
)
) {
lastResponse = httpClient.checkLicense(
firstAccount, accountManager, androidId, packageInfo, packageName, request
)
}
} else {
Log.d(TAG, "Not auto-purchasing $packageName as it is disabled by the user")
}
return lastResponse
}
private fun handleNoAccounts(packageName: String, packageManager: PackageManager) {
try {
Log.e(TAG, "not checking license, as user is not signed in")
packageManager.getPackageInfo(packageName, 0)!!.let {
sendLicenseServiceNotification(
packageName,
packageManager.getApplicationLabel(it.applicationInfo!!),
it.applicationInfo!!.uid
)
}
} catch (e: PackageManager.NameNotFoundException) {
Log.e(TAG, "ignored license request, but package name $packageName was not known!")
// don't send sign in notification
} catch (e: NullPointerException) {
Log.e(TAG, "ignored license request, but couldn't get package info for $packageName")
}
}
}
override fun onBind(intent: Intent): IBinder {
ensureInitialized(this)
contentResolver.query(
CHECKIN_SETTINGS_PROVIDER,
arrayOf("androidId"),
null,
null,
null
).use { cursor ->
if (cursor != null) {
cursor.moveToNext()
androidId = java.lang.Long.toHexString(cursor.getLong(0))
}
}
accountManager = AccountManager.get(this)
httpClient = HttpClient()
return mLicenseService
}
companion object {
private const val TAG = "FakeLicenseService"
private const val KEY_V2_RESULT_JWT = "LICENSE_DATA"
private val CHECKIN_SETTINGS_PROVIDER: Uri =
Uri.parse("content://com.google.android.gms.microg.settings/check-in")
}
}

View file

@ -0,0 +1,37 @@
/*
* SPDX-FileCopyrightText: 2023 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.finsky.externalreferrer;
import android.app.Service;
import android.content.Intent;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
public class GetInstallReferrerService extends Service {
private static final String TAG = "FakeReferrerService";
private final IGetInstallReferrerService.Stub service = new IGetInstallReferrerService.Stub() {
// https://developer.android.com/google/play/installreferrer/igetinstallreferrerservice
@Override
public Bundle getInstallReferrer(Bundle request) throws RemoteException {
Bundle result = new Bundle();
result.putString("install_referrer", "utm_source=google-play&utm_medium=organic");
result.putLong("referrer_click_timestamp_seconds", 0);
result.putLong("referrer_click_timestamp_server_seconds", 0);
result.putLong("install_begin_timestamp_seconds", 0);
result.putLong("install_begin_timestamp_server_seconds", 0);
result.putString("install_version", null);
result.putBoolean("google_play_instant", false);
return result;
}
};
@Override
public IBinder onBind(Intent intent) {
return service.asBinder();
}
}

View file

@ -0,0 +1,36 @@
/*
* SPDX-FileCopyrightText: 2024 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.play.core.assetpacks;
import com.google.android.play.core.assetpacks.model.AssetPackStorageMethod;
/**
* Location of a single asset, belonging to an asset pack.
* <p>
* If the AssetPackStorageMethod for the pack is {@link AssetPackStorageMethod#APK_ASSETS}, this will be the path to the
* APK containing the asset, the offset of the asset inside the APK and the size of the asset. The asset file will be
* uncompressed, unless `bundletool` has been explicitly configured to compress the asset pack.
* <p>
* If the AssetPackStorageMethod for the pack is {@link AssetPackStorageMethod#STORAGE_FILES}, this will be the path to
* the specific asset, the offset will be 0 and the size will be the size of the asset file. The asset file will be
* uncompressed.
*/
public abstract class AssetLocation {
/**
* Returns the file offset where the asset starts, in bytes.
*/
public abstract long offset();
/**
* Returns the path to the file containing the asset.
*/
public abstract String path();
/**
* Returns the size of the asset, in bytes.
*/
public abstract long size();
}

View file

@ -0,0 +1,40 @@
/*
* SPDX-FileCopyrightText: 2024 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.play.core.assetpacks;
import com.google.android.gms.common.api.ApiException;
import com.google.android.gms.common.api.Status;
import com.google.android.play.core.assetpacks.model.AssetPackErrorCode;
import org.microg.gms.common.Hide;
/**
* An exception indicating something went wrong with the Asset Delivery API.
* <p>
* See {@link #getErrorCode()} for the specific problem.
*/
public class AssetPackException extends ApiException {
@Hide
public AssetPackException(@AssetPackErrorCode int errorCode) {
super(new Status(errorCode, "Asset Pack Download Error(" + errorCode + ")"));
}
/**
* Returns an error code value from {@link AssetPackErrorCode}.
*/
@AssetPackErrorCode
public int getErrorCode() {
return super.getStatusCode();
}
/**
* Returns the error code resulting from the operation. The value is one of the constants in {@link AssetPackErrorCode}.
* getStatusCode() is unsupported by AssetPackException, please use getErrorCode() instead.
*/
@Override
public int getStatusCode() {
return super.getStatusCode();
}
}

View file

@ -0,0 +1,46 @@
/*
* SPDX-FileCopyrightText: 2024 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.play.core.assetpacks;
import androidx.annotation.Nullable;
import com.google.android.play.core.assetpacks.model.AssetPackStorageMethod;
/**
* Location of an asset pack on the device.
*/
public abstract class AssetPackLocation {
/**
* Returns the file path to the folder containing the pack's assets, if the storage method is
* {@link AssetPackStorageMethod#STORAGE_FILES}.
* <p>
* The files found at this path should not be modified.
* <p>
* If the storage method is {@link AssetPackStorageMethod#APK_ASSETS}, this method will return {@code null}. To access assets
* from packs installed as APKs, use Asset Manager.
*/
@Nullable
public abstract String assetsPath();
/**
* Returns whether the pack is installed as an APK or extracted into a folder on the filesystem.
*
* @return a value from {@link AssetPackStorageMethod}
*/
@AssetPackStorageMethod
public abstract int packStorageMethod();
/**
* Returns the file path to the folder containing the extracted asset pack, if the storage method is
* {@link AssetPackStorageMethod#STORAGE_FILES}.
* <p>
* The files found at this path should not be modified.
* <p>
* If the storage method is {@link AssetPackStorageMethod#APK_ASSETS}, this method will return {@code null}. To access assets
* from packs installed as APKs, use Asset Manager.
*/
@Nullable
public abstract String path();
}

View file

@ -0,0 +1,194 @@
/*
* SPDX-FileCopyrightText: 2024 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.play.core.assetpacks;
import android.app.Activity;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.IntentSenderRequest;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.gms.tasks.Task;
import com.google.android.play.core.assetpacks.model.AssetPackStatus;
import java.util.List;
import java.util.Map;
/**
* Manages downloads of asset packs.
*/
public interface AssetPackManager {
/**
* Requests to cancel the download of the specified asset packs.
* <p>
* Note: Only active downloads can be canceled.
*
* @return The new state for all specified packs.
*/
AssetPackStates cancel(@NonNull List<String> packNames);
/**
* Unregisters all listeners previously added using {@link #registerListener}.
*/
void clearListeners();
/**
* Requests to download the specified asset packs.
* <p>
* This method will fail if the app is not in the foreground.
*
* @return the state of all specified pack names
*/
Task<AssetPackStates> fetch(List<String> packNames);
/**
* [advanced API] Returns the location of an asset in a pack, or {@code null} if the asset is not present in the given pack.
* <p>
* You don't need to use this API for common use-cases: you can use the standard File API for accessing assets from
* asset packs that were extracted into the filesystem; and you can use Android's AssetManager API to access assets
* from packs that were installed as APKs.
* <p>
* This API is useful for game engines that don't use Asset Manager and for developers that want a unified method to
* access assets, independently from the delivery mode.
*/
@Nullable
AssetLocation getAssetLocation(@NonNull String packName, @NonNull String assetPath);
/**
* Returns the location of the specified asset pack on the device or {@code null} if this pack is not downloaded.
* <p>
* The files found at this path should not be modified.
*/
@Nullable
AssetPackLocation getPackLocation(@NonNull String packName);
/**
* Returns the location of all installed asset packs as a mapping from the asset pack name to an {@link AssetPackLocation}.
* <p>
* The files found at these paths should not be modified.
*/
Map<String, AssetPackLocation> getPackLocations();
/**
* Requests download state or details for the specified asset packs.
* <p>
* Do not use this method to determine whether an asset pack is downloaded. Instead use {@link #getPackLocation}.
*/
Task<AssetPackStates> getPackStates(List<String> packNames);
/**
* Registers a listener that will be notified of changes to the state of pack downloads for this app. Listeners should be
* subsequently unregistered using {@link #unregisterListener}.
*/
void registerListener(@NonNull AssetPackStateUpdateListener listener);
/**
* Deletes the specified asset pack from the internal storage of the app.
* <p>
* Use this method to delete asset packs instead of deleting files manually. This ensures that the Asset Pack will not be
* re-downloaded during an app update.
* <p>
* If the asset pack is currently being downloaded or installed, this method does not cancel the process. For this case,
* use {@link #cancel} instead.
*
* @return A task that will be successful only if files were successfully deleted.
*/
Task<Void> removePack(@NonNull String packName);
/**
* Shows a confirmation dialog to resume all pack downloads that are currently in the
* {@link AssetPackStatus#WAITING_FOR_WIFI} state. If the user accepts the dialog, packs are downloaded over cellular data.
* <p>
* The status of an asset pack is set to {@link AssetPackStatus#WAITING_FOR_WIFI} if the user is currently not on a Wi-Fi
* connection and the asset pack is large or the user has set their download preference in the Play Store to only
* download apps over Wi-Fi. By showing this dialog, your app can ask the user if they accept downloading the asset
* pack over cellular data instead of waiting for Wi-Fi.
* <p>
* The confirmation activity returns one of the following values:
* <ul>
* <li>{@link Activity#RESULT_OK Activity#RESULT_OK} if the user accepted.
* <li>{@link Activity#RESULT_CANCELED Activity#RESULT_CANCELED} if the user denied or the dialog has been closed in any other way (e.g.
* backpress).
* </ul>
*
* @param activityResultLauncher an activityResultLauncher to launch the confirmation dialog.
* @return whether the confirmation dialog has been started.
* @deprecated This API has been deprecated in favor of {@link #showConfirmationDialog(ActivityResultLauncher)}.
*/
@Deprecated
boolean showCellularDataConfirmation(@NonNull ActivityResultLauncher<IntentSenderRequest> activityResultLauncher);
/**
* Shows a confirmation dialog to resume all pack downloads that are currently in the
* {@link AssetPackStatus#WAITING_FOR_WIFI} state. If the user accepts the dialog, packs are downloaded over cellular data.
* <p>
* The status of an asset pack is set to {@link AssetPackStatus#WAITING_FOR_WIFI} if the user is currently not on a Wi-Fi
* connection and the asset pack is large or the user has set their download preference in the Play Store to only
* download apps over Wi-Fi. By showing this dialog, your app can ask the user if they accept downloading the asset
* pack over cellular data instead of waiting for Wi-Fi.
*
* @param activity the activity on top of which the confirmation dialog is displayed. Use your current
* activity for this.
* @return A {@link Task} that completes once the dialog has been accepted, denied or closed. A successful task
* result contains one of the following values:
* <ul>
* <li>{@link Activity#RESULT_OK Activity#RESULT_OK} if the user accepted.
* <li>{@link Activity#RESULT_CANCELED Activity#RESULT_CANCELED} if the user denied or the dialog has been closed in any other way (e.g.
* backpress).
* </ul>
* @deprecated This API has been deprecated in favor of {@link #showConfirmationDialog(Activity)}.
*/
@Deprecated
Task<Integer> showCellularDataConfirmation(@NonNull Activity activity);
/**
* Shows a dialog that asks the user for consent to download packs that are currently in either the
* {@link AssetPackStatus#REQUIRES_USER_CONFIRMATION} state or the {@link AssetPackStatus#WAITING_FOR_WIFI} state.
* <p>
* If the app has not been installed by Play, an update may be triggered to ensure that a valid version is installed. This
* will cause the app to restart and all asset requests to be cancelled. These assets should be requested again after the
* app restarts.
* <p>
* The confirmation activity returns one of the following values:
* <ul>
* <li>{@link Activity#RESULT_OK Activity#RESULT_OK} if the user accepted.
* <li>{@link Activity#RESULT_CANCELED Activity#RESULT_CANCELED} if the user denied or the dialog has been closed in any other way (e.g.
* backpress).
* </ul>
*
* @param activityResultLauncher an activityResultLauncher to launch the confirmation dialog.
* @return whether the confirmation dialog has been started.
*/
boolean showConfirmationDialog(@NonNull ActivityResultLauncher<IntentSenderRequest> activityResultLauncher);
/**
* Shows a dialog that asks the user for consent to download packs that are currently in either the
* {@link AssetPackStatus#REQUIRES_USER_CONFIRMATION} state or the {@link AssetPackStatus#WAITING_FOR_WIFI} state.
* <p>
* If the app has not been installed by Play, an update may be triggered to ensure that a valid version is installed. This
* will cause the app to restart and all asset requests to be cancelled. These assets should be requested again after the
* app restarts.
*
* @param activity the activity on top of which the confirmation dialog is displayed. Use your current
* activity for this.
* @return A {@link Task} that completes once the dialog has been accepted, denied or closed. A successful task
* result contains one of the following values:
* <ul>
* <li>{@link Activity#RESULT_OK Activity#RESULT_OK} if the user accepted.
* <li>{@link Activity#RESULT_CANCELED Activity#RESULT_CANCELED} if the user denied or the dialog has been closed in any other way (e.g.
* backpress).
* </ul>
*/
Task<Integer> showConfirmationDialog(@NonNull Activity activity);
/**
* Unregisters a listener previously added using {@link #registerListener}.
*/
void unregisterListener(@NonNull AssetPackStateUpdateListener listener);
}

View file

@ -0,0 +1,27 @@
/*
* SPDX-FileCopyrightText: 2024 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.play.core.assetpacks;
import android.content.Context;
import androidx.annotation.NonNull;
/**
* Creates instances of {@link AssetPackManager}.
*/
public final class AssetPackManagerFactory {
private AssetPackManagerFactory() {
}
/**
* Creates an instance of {@link AssetPackManager}.
*
* @param applicationContext a fully initialized application context
*/
@NonNull
public static AssetPackManager getInstance(Context applicationContext) {
return new AssetPackManagerImpl();
}
}

View file

@ -0,0 +1,104 @@
/*
* SPDX-FileCopyrightText: 2024 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.play.core.assetpacks;
import android.app.Activity;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.IntentSenderRequest;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.gms.tasks.Task;
import com.google.android.play.core.assetpacks.model.AssetPackStatus;
import org.microg.gms.common.Hide;
import java.util.List;
import java.util.Map;
@Hide
public class AssetPackManagerImpl implements AssetPackManager {
@Override
public AssetPackStates cancel(@NonNull List<String> packNames) {
throw new UnsupportedOperationException();
}
@Override
public void clearListeners() {
}
@Override
public Task<AssetPackStates> fetch(List<String> packNames) {
throw new UnsupportedOperationException();
}
@Nullable
@Override
public AssetLocation getAssetLocation(@NonNull String packName, @NonNull String assetPath) {
throw new UnsupportedOperationException();
}
@Nullable
@Override
public AssetPackLocation getPackLocation(@NonNull String packName) {
throw new UnsupportedOperationException();
}
@Override
public Map<String, AssetPackLocation> getPackLocations() {
throw new UnsupportedOperationException();
}
@Override
public Task<AssetPackStates> getPackStates(List<String> packNames) {
throw new UnsupportedOperationException();
}
@Override
public void registerListener(@NonNull AssetPackStateUpdateListener listener) {
throw new UnsupportedOperationException();
}
@Override
public Task<Void> removePack(@NonNull String packName) {
throw new UnsupportedOperationException();
}
@Override
public boolean showCellularDataConfirmation(@NonNull ActivityResultLauncher<IntentSenderRequest> activityResultLauncher) {
throw new UnsupportedOperationException();
}
@Override
public Task<Integer> showCellularDataConfirmation(@NonNull Activity activity) {
throw new UnsupportedOperationException();
}
@Override
public boolean showConfirmationDialog(@NonNull ActivityResultLauncher<IntentSenderRequest> activityResultLauncher) {
throw new UnsupportedOperationException();
}
@Override
public Task<Integer> showConfirmationDialog(@NonNull Activity activity) {
throw new UnsupportedOperationException();
}
@Override
public void unregisterListener(@NonNull AssetPackStateUpdateListener listener) {
}
public @AssetPackStatus int getLocalStatus(String packName, int remoteStatus) {
throw new UnsupportedOperationException();
}
public int getTransferProgressPercentage(String packName) {
throw new UnsupportedOperationException();
}
public String getInstalledVersionTag(String packName) {
throw new UnsupportedOperationException();
}
}

View file

@ -0,0 +1,366 @@
/*
* SPDX-FileCopyrightText: 2024 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.play.core.assetpacks;
import android.content.Context;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.util.Log;
import androidx.annotation.NonNull;
import com.google.android.gms.tasks.Task;
import com.google.android.gms.tasks.TaskCompletionSource;
import com.google.android.play.core.assetpacks.model.AssetPackErrorCode;
import com.google.android.play.core.assetpacks.model.AssetPackStatus;
import com.google.android.play.core.assetpacks.protocol.*;
import org.microg.gms.common.Hide;
import java.util.*;
@Hide
public class AssetPackServiceClient {
private static final String TAG = "AssetPackServiceClient";
private List<TaskCompletionSource<?>> pendingCalls = new ArrayList<>();
private Context context;
private AssetPackManagerImpl assetPackManager;
private interface PendingCall<TResult> {
void execute(IAssetModuleService service, TaskCompletionSource<TResult> completionSource) throws Exception;
}
private <TResult> Task<TResult> execute(PendingCall<TResult> pendingCall) {
TaskCompletionSource<TResult> completionSource = new TaskCompletionSource<>();
pendingCalls.add(completionSource);
try {
pendingCall.execute(null, completionSource);
} catch (Exception e) {
completionSource.trySetException(e);
}
Task<TResult> task = completionSource.getTask();
task.addOnCompleteListener(ignored -> pendingCalls.remove(completionSource));
return task;
}
private Bundle getOptionsBundle() {
Bundle options = new Bundle();
// TODO
BundleKeys.put(options, BundleKeys.PLAY_CORE_VERSION_CODE, 20202);
BundleKeys.put(options, BundleKeys.SUPPORTED_COMPRESSION_FORMATS, new ArrayList<>(Arrays.asList(CompressionFormat.UNSPECIFIED, CompressionFormat.BROTLI)));
BundleKeys.put(options, BundleKeys.SUPPORTED_PATCH_FORMATS, new ArrayList<>(Arrays.asList(PatchFormat.PATCH_GDIFF, PatchFormat.GZIPPED_GDIFF)));
return options;
}
private ArrayList<Bundle> getModuleNameBundles(List<String> packNames) {
ArrayList<Bundle> moduleNameBundles = new ArrayList<>();
for (String packName : packNames) {
Bundle arg = new Bundle();
BundleKeys.put(arg, BundleKeys.MODULE_NAME, packName);
moduleNameBundles.add(arg);
}
return moduleNameBundles;
}
private Bundle getInstalledAssetModulesBundle(Map<String, Long> installedAssetModules) {
Bundle installedAssetModulesBundle = getOptionsBundle();
ArrayList<Bundle> installedAssetModuleBundles = new ArrayList<>();
for (String moduleName : installedAssetModules.keySet()) {
Bundle installedAssetModuleBundle = new Bundle();
BundleKeys.put(installedAssetModuleBundle, BundleKeys.INSTALLED_ASSET_MODULE_NAME, moduleName);
BundleKeys.put(installedAssetModuleBundle, BundleKeys.INSTALLED_ASSET_MODULE_VERSION, installedAssetModules.get(moduleName));
installedAssetModuleBundles.add(installedAssetModuleBundle);
}
BundleKeys.put(installedAssetModulesBundle, BundleKeys.INSTALLED_ASSET_MODULE, installedAssetModuleBundles);
return installedAssetModulesBundle;
}
private Bundle getSessionIdentifierBundle(int sessionId) {
Bundle sessionIdentifierBundle = new Bundle();
BundleKeys.put(sessionIdentifierBundle, BundleKeys.SESSION_ID, sessionId);
return sessionIdentifierBundle;
}
private Bundle getModuleIdentifierBundle(int sessionId, String moduleName) {
Bundle moduleIdentifierBundle = getSessionIdentifierBundle(sessionId);
BundleKeys.put(moduleIdentifierBundle, BundleKeys.MODULE_NAME, moduleName);
return moduleIdentifierBundle;
}
private Bundle getChunkIdentifierBundle(int sessionId, String moduleName, String sliceId, int chunkNumber) {
Bundle chunkIdentifierBundle = getModuleIdentifierBundle(sessionId, moduleName);
BundleKeys.put(chunkIdentifierBundle, BundleKeys.SLICE_ID, sliceId);
BundleKeys.put(chunkIdentifierBundle, BundleKeys.CHUNK_NUMBER, chunkNumber);
return chunkIdentifierBundle;
}
public Task<ParcelFileDescriptor> getChunkFileDescriptor(int sessionId, String moduleName, String sliceId, int chunkNumber) {
return execute((service, completionSource) -> {
service.getChunkFileDescriptor(context.getPackageName(), getChunkIdentifierBundle(sessionId, moduleName, sliceId, chunkNumber), getOptionsBundle(), new BaseCallback(completionSource) {
@Override
public void onGetChunkFileDescriptor(ParcelFileDescriptor chunkFileDescriptor) {
completionSource.trySetResult(chunkFileDescriptor);
}
});
});
}
public Task<AssetPackStates> getPackStates(List<String> packNames, Map<String, Long> installedAssetModules) {
return execute((service, completionSource) -> {
service.requestDownloadInfo(context.getPackageName(), getModuleNameBundles(packNames), getInstalledAssetModulesBundle(installedAssetModules), new BaseCallback(completionSource) {
@Override
public void onRequestDownloadInfo(Bundle bundle, Bundle bundle2) {
completionSource.trySetResult(AssetPackStatesImpl.fromBundle(bundle, assetPackManager));
}
});
});
}
public Task<AssetPackStates> startDownload(List<String> packNames, Map<String, Long> installedAssetModules) {
Task<AssetPackStates> task = execute((service, completionSource) -> {
service.startDownload(context.getPackageName(), getModuleNameBundles(packNames), getInstalledAssetModulesBundle(installedAssetModules), new BaseCallback(completionSource) {
@Override
public void onStartDownload(int status, Bundle bundle) {
completionSource.trySetResult(AssetPackStatesImpl.fromBundle(bundle, assetPackManager, true));
}
});
});
task.addOnSuccessListener(ignored -> keepAlive());
return task;
}
public Task<List<String>> syncPacks(Map<String, Long> installedAssetModules) {
return execute((service, completionSource) -> {
service.getSessionStates(context.getPackageName(), getInstalledAssetModulesBundle(installedAssetModules), new BaseCallback(completionSource) {
@Override
public void onGetSessionStates(List<Bundle> list) {
ArrayList<String> packNames = new ArrayList<>();
for (Bundle bundle : list) {
Collection<AssetPackState> packStates = AssetPackStatesImpl.fromBundle(bundle, assetPackManager, true).packStates().values();
if (!packStates.isEmpty()) {
AssetPackState state = packStates.iterator().next();
switch (state.status()) {
case AssetPackStatus.PENDING:
case AssetPackStatus.DOWNLOADING:
case AssetPackStatus.TRANSFERRING:
case AssetPackStatus.WAITING_FOR_WIFI:
case AssetPackStatus.REQUIRES_USER_CONFIRMATION:
packNames.add(state.name());
}
}
}
completionSource.trySetResult(packNames);
}
});
});
}
public void cancelDownloads(List<String> packNames) {
execute((service, completionSource) -> {
service.cancelDownloads(context.getPackageName(), getModuleNameBundles(packNames), getOptionsBundle(), new BaseCallback(completionSource) {
@Override
public void onCancelDownloads() {
completionSource.trySetResult(null);
}
});
});
}
public void keepAlive() {
// TODO
}
public void notifyChunkTransferred(int sessionId, String moduleName, String sliceId, int chunkNumber) {
execute((service, completionSource) -> {
service.notifyChunkTransferred(context.getPackageName(), getChunkIdentifierBundle(sessionId, moduleName, sliceId, chunkNumber), getOptionsBundle(), new BaseCallback(completionSource) {
@Override
public void onNotifyChunkTransferred(int sessionId, String moduleName, String sliceId, int chunkNumber) {
completionSource.trySetResult(null);
}
});
});
}
public void notifyModuleCompleted(int sessionId, String moduleName) {
notifyModuleCompleted(sessionId, moduleName, 10);
}
public void notifyModuleCompleted(int sessionId, String moduleName, int maxRetries) {
execute((service, completionSource) -> {
service.notifyModuleCompleted(context.getPackageName(), getModuleIdentifierBundle(sessionId, moduleName), getOptionsBundle(), new BaseCallback(completionSource) {
@Override
public void onError(int errorCode) {
if (maxRetries > 0) {
notifyModuleCompleted(sessionId, moduleName, maxRetries - 1);
}
}
});
});
}
public void notifySessionFailed(int sessionId) {
execute((service, completionSource) -> {
service.notifySessionFailed(context.getPackageName(), getSessionIdentifierBundle(sessionId), getOptionsBundle(), new BaseCallback(completionSource) {
@Override
public void onNotifySessionFailed(int sessionId) {
completionSource.trySetResult(null);
}
});
});
}
public void removePack(String packName) {
execute((service, completionSource) -> {
service.removeModule(context.getPackageName(), getModuleIdentifierBundle(0, packName), getOptionsBundle(), new BaseCallback(completionSource) {
@Override
public void onRemoveModule() {
completionSource.trySetResult(null);
}
});
});
}
private static class BaseCallback extends IAssetModuleServiceCallback.Stub {
@NonNull
private final TaskCompletionSource<?> completionSource;
public BaseCallback(@NonNull TaskCompletionSource<?> completionSource) {
this.completionSource = completionSource;
}
@Override
public void onStartDownload(int sessionId, Bundle bundle) {
Log.i(TAG, "onStartDownload(" + sessionId + ")");
onStartDownload(sessionId);
}
public void onStartDownload(int sessionId) {
completionSource.trySetException(new Exception("Unexpected callback: onStartDownload"));
}
@Override
public void onCancelDownload(int status, Bundle bundle) {
Log.i(TAG, "onCancelDownload(" + status + ")");
onCancelDownload(status);
}
public void onCancelDownload(int status) {
completionSource.trySetException(new Exception("Unexpected callback: onCancelDownload"));
}
@Override
public void onGetSession(int status, Bundle bundle) {
Log.i(TAG, "onGetSession(" + status + ")");
onGetSession(status);
}
public void onGetSession(int status) {
completionSource.trySetException(new Exception("Unexpected callback: onGetSession"));
}
@Override
public void onGetSessionStates(List<Bundle> list) {
completionSource.trySetException(new Exception("Unexpected callback: onGetSessionStates"));
}
@Override
public void onNotifyChunkTransferred(Bundle bundle, Bundle bundle2) {
int sessionId = BundleKeys.get(bundle, BundleKeys.SESSION_ID, 0);
String moduleName = BundleKeys.get(bundle, BundleKeys.MODULE_NAME);
String sliceId = BundleKeys.get(bundle, BundleKeys.SLICE_ID);
int chunkNumber = BundleKeys.get(bundle, BundleKeys.CHUNK_NUMBER, 0);
Log.i(TAG, "onNotifyChunkTransferred(" + sessionId + ", " + moduleName + ", " + sliceId + ", " + chunkNumber + ")");
onNotifyChunkTransferred(sessionId, moduleName, sliceId, chunkNumber);
}
public void onNotifyChunkTransferred(int sessionId, String moduleName, String sliceId, int chunkNumber) {
completionSource.trySetException(new Exception("Unexpected callback: onNotifyChunkTransferred"));
}
@Override
public void onError(Bundle bundle) {
int errorCode = BundleKeys.get(bundle, BundleKeys.ERROR_CODE, AssetPackErrorCode.INTERNAL_ERROR);
onError(errorCode);
}
public void onError(int errorCode) {
completionSource.trySetException(new AssetPackException(errorCode));
}
@Override
public void onNotifyModuleCompleted(Bundle bundle, Bundle bundle2) {
int sessionId = BundleKeys.get(bundle, BundleKeys.SESSION_ID, 0);
String moduleName = BundleKeys.get(bundle, BundleKeys.MODULE_NAME);
Log.i(TAG, "onNotifyModuleCompleted(" + sessionId + ", " + moduleName + ")");
onNotifyModuleCompleted(sessionId, moduleName);
}
public void onNotifyModuleCompleted(int sessionId, String moduleName) {
completionSource.trySetException(new Exception("Unexpected callback: onNotifyModuleCompleted"));
}
@Override
public void onNotifySessionFailed(Bundle bundle) {
int sessionId = BundleKeys.get(bundle, BundleKeys.SESSION_ID, 0);
Log.i(TAG, "onNotifySessionFailed(" + sessionId + ")");
onNotifySessionFailed(sessionId);
}
public void onNotifySessionFailed(int sessionId) {
completionSource.trySetException(new Exception("Unexpected callback: onNotifySessionFailed"));
}
@Override
public void onKeepAlive(Bundle bundle, Bundle bundle2) {
boolean keepAlive = BundleKeys.get(bundle, BundleKeys.KEEP_ALIVE, false);
Log.i(TAG, "onKeepAlive(" + keepAlive + ")");
onKeepAlive(keepAlive);
}
public void onKeepAlive(boolean keepAlive) {
completionSource.trySetException(new Exception("Unexpected callback: onKeepAlive"));
}
@Override
public void onGetChunkFileDescriptor(Bundle bundle, Bundle bundle2) {
ParcelFileDescriptor chunkFileDescriptor = BundleKeys.get(bundle, BundleKeys.CHUNK_FILE_DESCRIPTOR);
Log.i(TAG, "onGetChunkFileDescriptor(...)");
onGetChunkFileDescriptor(chunkFileDescriptor);
}
public void onGetChunkFileDescriptor(ParcelFileDescriptor chunkFileDescriptor) {
completionSource.trySetException(new Exception("Unexpected callback: onGetChunkFileDescriptor"));
}
@Override
public void onRequestDownloadInfo(Bundle bundle, Bundle bundle2) {
Log.i(TAG, "onRequestDownloadInfo()");
onRequestDownloadInfo();
}
public void onRequestDownloadInfo() {
completionSource.trySetException(new Exception("Unexpected callback: onRequestDownloadInfo"));
}
@Override
public void onRemoveModule(Bundle bundle, Bundle bundle2) {
Log.i(TAG, "onRemoveModule()");
onRemoveModule();
}
public void onRemoveModule() {
completionSource.trySetException(new Exception("Unexpected callback: onRemoveModule"));
}
@Override
public void onCancelDownloads(Bundle bundle) {
Log.i(TAG, "onCancelDownload()");
onCancelDownloads();
}
public void onCancelDownloads() {
completionSource.trySetException(new Exception("Unexpected callback: onCancelDownloads"));
}
}
}

View file

@ -0,0 +1,65 @@
/*
* SPDX-FileCopyrightText: 2024 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.play.core.assetpacks;
import com.google.android.play.core.assetpacks.model.AssetPackErrorCode;
import com.google.android.play.core.assetpacks.model.AssetPackStatus;
import com.google.android.play.core.assetpacks.model.AssetPackUpdateAvailability;
/**
* The state of an individual asset pack.
*/
public abstract class AssetPackState {
public abstract String availableVersionTag();
/**
* Returns the total number of bytes already downloaded for the pack.
*/
public abstract long bytesDownloaded();
/**
* Returns the error code for the pack, if Play has failed to download the pack. Returns
* {@link AssetPackErrorCode#NO_ERROR} if the download was successful or is in progress or has not been attempted.
*
* @return A value from {@link AssetPackErrorCode}.
*/
@AssetPackErrorCode
public abstract int errorCode();
public abstract String installedVersionTag();
/**
* Returns the name of the pack.
*/
public abstract String name();
/**
* Returns the download status of the pack.
* <p>
* If the pack has never been requested before its status is {@link AssetPackStatus#UNKNOWN}.
*
* @return a value from {@link AssetPackStatus}
*/
@AssetPackStatus
public abstract int status();
/**
* Returns the total size of the pack in bytes.
*/
public abstract long totalBytesToDownload();
/**
* Returns the percentage of the asset pack already transferred to the app.
* <p>
* This value is only defined when the status is {@link AssetPackStatus#TRANSFERRING}.
*
* @return a value between 0 and 100 inclusive.
*/
public abstract int transferProgressPercentage();
@AssetPackUpdateAvailability
public abstract int updateAvailability();
}

View file

@ -0,0 +1,130 @@
/*
* SPDX-FileCopyrightText: 2024 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.play.core.assetpacks;
import android.os.Bundle;
import androidx.annotation.NonNull;
import com.google.android.play.core.assetpacks.model.AssetPackErrorCode;
import com.google.android.play.core.assetpacks.model.AssetPackStatus;
import com.google.android.play.core.assetpacks.model.AssetPackUpdateAvailability;
import com.google.android.play.core.assetpacks.protocol.BundleKeys;
import org.microg.gms.common.Hide;
import org.microg.gms.utils.ToStringHelper;
@Hide
public class AssetPackStateImpl extends AssetPackState {
private final String name;
private final @AssetPackStatus int status;
private final @AssetPackErrorCode int errorCode;
private final long bytesDownloaded;
private final long totalBytesToDownload;
private final int transferProgressPercentage;
@AssetPackUpdateAvailability
private final int updateAvailability;
private final String availableVersionTag;
private final String installedVersionTag;
public AssetPackStateImpl(String name, @AssetPackStatus int status, @AssetPackErrorCode int errorCode, long bytesDownloaded, long totalBytesToDownload, int transferProgressPercentage, @AssetPackUpdateAvailability int updateAvailability, String availableVersionTag, String installedVersionTag) {
this.name = name;
this.status = status;
this.errorCode = errorCode;
this.bytesDownloaded = bytesDownloaded;
this.totalBytesToDownload = totalBytesToDownload;
this.transferProgressPercentage = transferProgressPercentage;
this.updateAvailability = updateAvailability;
this.availableVersionTag = availableVersionTag;
this.installedVersionTag = installedVersionTag;
}
@NonNull
public static AssetPackState fromBundle(Bundle bundle, @NonNull String name, AssetPackManagerImpl assetPackManager) {
return fromBundle(bundle, name, assetPackManager, false);
}
@NonNull
public static AssetPackState fromBundle(Bundle bundle, @NonNull String name, AssetPackManagerImpl assetPackManager, boolean ignoreLocalStatus) {
@AssetPackStatus int remoteStatus = BundleKeys.get(bundle, BundleKeys.STATUS, name, 0);
@AssetPackStatus int status = ignoreLocalStatus ? remoteStatus : assetPackManager.getLocalStatus(name, remoteStatus);
@AssetPackErrorCode int errorCode = BundleKeys.get(bundle, BundleKeys.ERROR_CODE, name, 0);
long bytesDownloaded = BundleKeys.get(bundle, BundleKeys.BYTES_DOWNLOADED, name, 0L);
long totalBytesToDownload = BundleKeys.get(bundle, BundleKeys.TOTAL_BYTES_TO_DOWNLOAD, name, 0L);
int transferProgressPercentage = assetPackManager.getTransferProgressPercentage(name);
long packVersion = BundleKeys.get(bundle, BundleKeys.PACK_VERSION, name, 0L);
long packBaseVersion = BundleKeys.get(bundle, BundleKeys.PACK_BASE_VERSION, name, 0L);
int appVersionCode = BundleKeys.get(bundle, BundleKeys.APP_VERSION_CODE, 0);
String availableVersionTag = BundleKeys.get(bundle, BundleKeys.PACK_VERSION_TAG, name, Integer.toString(appVersionCode));
String installedVersionTag = assetPackManager.getInstalledVersionTag(name);
int updateAvailability = AssetPackUpdateAvailability.UPDATE_NOT_AVAILABLE;
if (status == AssetPackStatus.COMPLETED && packBaseVersion != 0 && packBaseVersion != packVersion) {
updateAvailability = AssetPackUpdateAvailability.UPDATE_AVAILABLE;
}
return new AssetPackStateImpl(name, status, errorCode, bytesDownloaded, totalBytesToDownload, transferProgressPercentage, updateAvailability, availableVersionTag, installedVersionTag);
}
@Override
public String availableVersionTag() {
return availableVersionTag;
}
@Override
public long bytesDownloaded() {
return bytesDownloaded;
}
@Override
@AssetPackErrorCode
public int errorCode() {
return errorCode;
}
@Override
public String installedVersionTag() {
return installedVersionTag;
}
@Override
public String name() {
return name;
}
@Override
@AssetPackStatus
public int status() {
return status;
}
@Override
public long totalBytesToDownload() {
return totalBytesToDownload;
}
@Override
public int transferProgressPercentage() {
return transferProgressPercentage;
}
@Override
@AssetPackUpdateAvailability
public int updateAvailability() {
return updateAvailability;
}
@NonNull
@Override
public String toString() {
return ToStringHelper.name("AssetPackState")
.field("name", name)
.field("status", status)
.field("errorCode", errorCode)
.field("bytesDownloaded", bytesDownloaded)
.field("totalBytesToDownload", totalBytesToDownload)
.field("transferProgressPercentage", transferProgressPercentage)
.field("updateAvailability", updateAvailability)
.field("availableVersionTag", availableVersionTag)
.field("installedVersionTag", installedVersionTag)
.end();
}
}

View file

@ -0,0 +1,14 @@
/*
* SPDX-FileCopyrightText: 2024 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.play.core.assetpacks;
import com.google.android.play.core.listener.StateUpdateListener;
/**
* Listener that may be registered for updates on the state of the download of asset packs.
*/
public interface AssetPackStateUpdateListener extends StateUpdateListener<AssetPackState> {
}

View file

@ -0,0 +1,23 @@
/*
* SPDX-FileCopyrightText: 2024 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.play.core.assetpacks;
import java.util.Map;
/**
* Contains the state for all requested packs.
*/
public abstract class AssetPackStates {
/**
* Returns a map from a pack's name to its state.
*/
public abstract Map<String, AssetPackState> packStates();
/**
* Returns total size of all requested packs in bytes.
*/
public abstract long totalBytes();
}

View file

@ -0,0 +1,65 @@
/*
* SPDX-FileCopyrightText: 2024 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.play.core.assetpacks;
import android.os.Bundle;
import androidx.annotation.NonNull;
import com.google.android.play.core.assetpacks.protocol.BundleKeys;
import org.microg.gms.common.Hide;
import org.microg.gms.utils.ToStringHelper;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
@Hide
public class AssetPackStatesImpl extends AssetPackStates {
private final long totalBytes;
@NonNull
private final Map<String, AssetPackState> packStates;
public AssetPackStatesImpl(long totalBytes, @NonNull Map<String, AssetPackState> packStates) {
this.totalBytes = totalBytes;
this.packStates = packStates;
}
public static AssetPackStates fromBundle(@NonNull Bundle bundle, @NonNull AssetPackManagerImpl assetPackManager) {
return fromBundle(bundle, assetPackManager, false);
}
@NonNull
public static AssetPackStates fromBundle(@NonNull Bundle bundle, @NonNull AssetPackManagerImpl assetPackManager, boolean ignoreLocalStatus) {
long totalBytes = BundleKeys.get(bundle, BundleKeys.TOTAL_BYTES_TO_DOWNLOAD, 0L);
ArrayList<String> packNames = BundleKeys.get(bundle, BundleKeys.PACK_NAMES);
Map<String, AssetPackState> packStates = new HashMap<>();
if (packNames != null) {
for (String packName : packNames) {
packStates.put(packName, AssetPackStateImpl.fromBundle(bundle, packName, assetPackManager, ignoreLocalStatus));
}
}
return new AssetPackStatesImpl(totalBytes, packStates);
}
@Override
@NonNull
public Map<String, AssetPackState> packStates() {
return packStates;
}
@Override
public long totalBytes() {
return totalBytes;
}
@NonNull
@Override
public String toString() {
return ToStringHelper.name("AssetPackStates")
.field("totalBytes", totalBytes)
.field("packStates", packStates)
.end();
}
}

View file

@ -0,0 +1,85 @@
/*
* SPDX-FileCopyrightText: 2024 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.play.core.assetpacks.model;
import android.app.Activity;
import androidx.annotation.IntDef;
import com.google.android.play.core.assetpacks.AssetPackManager;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Error codes for the download of an asset pack.
*/
@Target({ElementType.TYPE_USE})
@Retention(RetentionPolicy.CLASS)
@IntDef({AssetPackErrorCode.NO_ERROR, AssetPackErrorCode.APP_UNAVAILABLE, AssetPackErrorCode.PACK_UNAVAILABLE, AssetPackErrorCode.INVALID_REQUEST, AssetPackErrorCode.DOWNLOAD_NOT_FOUND, AssetPackErrorCode.API_NOT_AVAILABLE, AssetPackErrorCode.NETWORK_ERROR, AssetPackErrorCode.ACCESS_DENIED, AssetPackErrorCode.INSUFFICIENT_STORAGE, AssetPackErrorCode.APP_NOT_OWNED, AssetPackErrorCode.PLAY_STORE_NOT_FOUND, AssetPackErrorCode.NETWORK_UNRESTRICTED, AssetPackErrorCode.CONFIRMATION_NOT_REQUIRED, AssetPackErrorCode.UNRECOGNIZED_INSTALLATION, AssetPackErrorCode.INTERNAL_ERROR})
public @interface AssetPackErrorCode {
int NO_ERROR = 0;
/**
* The requesting app is unavailable.
*/
int APP_UNAVAILABLE = -1;
/**
* The requested asset pack isn't available.
* <p>
* This can happen if the asset pack wasn't included in the Android App Bundle that was published to the Play Store.
*/
int PACK_UNAVAILABLE = -2;
/**
* The request is invalid.
*/
int INVALID_REQUEST = -3;
/**
* The requested download isn't found.
*/
int DOWNLOAD_NOT_FOUND = -4;
/**
* The Asset Delivery API isn't available.
*/
int API_NOT_AVAILABLE = -5;
/**
* Network error. Unable to obtain the asset pack details.
*/
int NETWORK_ERROR = -6;
/**
* Download not permitted under the current device circumstances (e.g. in background).
*/
int ACCESS_DENIED = -7;
/**
* Asset pack download failed due to insufficient storage.
*/
int INSUFFICIENT_STORAGE = -10;
/**
* The Play Store app is either not installed or not the official version.
*/
int PLAY_STORE_NOT_FOUND = -11;
/**
* Returned if {@link AssetPackManager#showCellularDataConfirmation(Activity)} is called but no asset packs are
* waiting for Wi-Fi.
*/
int NETWORK_UNRESTRICTED = -12;
/**
* The app isn't owned by any user on this device. An app is "owned" if it has been installed via the Play Store.
*/
int APP_NOT_OWNED = -13;
/**
* Returned if {@link AssetPackManager#showConfirmationDialog(Activity)} is called but no asset packs require user
* confirmation.
*/
int CONFIRMATION_NOT_REQUIRED = -14;
/**
* The installed app version is not recognized by Play. This can happen if the app was not installed by Play.
*/
int UNRECOGNIZED_INSTALLATION = -15;
/**
* Unknown error downloading an asset pack.
*/
int INTERNAL_ERROR = -100;
}

View file

@ -0,0 +1,71 @@
/*
* SPDX-FileCopyrightText: 2024 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.play.core.assetpacks.model;
import android.app.Activity;
import androidx.annotation.IntDef;
import com.google.android.play.core.assetpacks.AssetPackManager;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Status of the download of an asset pack.
*/
@Target({ElementType.TYPE_USE})
@Retention(RetentionPolicy.CLASS)
@IntDef({AssetPackStatus.UNKNOWN, AssetPackStatus.PENDING, AssetPackStatus.DOWNLOADING, AssetPackStatus.TRANSFERRING, AssetPackStatus.COMPLETED, AssetPackStatus.FAILED, AssetPackStatus.CANCELED, AssetPackStatus.WAITING_FOR_WIFI, AssetPackStatus.NOT_INSTALLED, AssetPackStatus.REQUIRES_USER_CONFIRMATION})
public @interface AssetPackStatus {
/**
* The asset pack state is unknown.
*/
int UNKNOWN = 0;
/**
* The asset pack download is pending and will be processed soon.
*/
int PENDING = 1;
/**
* The asset pack download is in progress.
*/
int DOWNLOADING = 2;
/**
* The asset pack is being decompressed and copied (or patched) to the app's internal storage.
*/
int TRANSFERRING = 3;
/**
* The asset pack download and transfer is complete; the assets are available to the app.
*/
int COMPLETED = 4;
/**
* The asset pack download or transfer has failed.
*/
int FAILED = 5;
/**
* The asset pack download has been canceled by the user through the Play Store or the download notification.
*/
int CANCELED = 6;
/**
* The asset pack download is waiting for Wi-Fi to become available before proceeding.
* <p>
* The app can ask the user to download a session that is waiting for Wi-Fi over cellular data by using
* {@link AssetPackManager#showCellularDataConfirmation(Activity)}.
*/
int WAITING_FOR_WIFI = 7;
/**
* The asset pack is not installed.
*/
int NOT_INSTALLED = 8;
/**
* The asset pack requires user consent to be downloaded.
* <p>
* This can happen if the current app version was not installed by Play.
* <p>
* If the asset pack is also waiting for Wi-Fi, this state takes precedence.
*/
int REQUIRES_USER_CONFIRMATION = 9;
}

View file

@ -0,0 +1,34 @@
/*
* SPDX-FileCopyrightText: 2024 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.play.core.assetpacks.model;
import androidx.annotation.IntDef;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Method used to store an asset pack.
*/
@Target({ElementType.TYPE_USE})
@Retention(RetentionPolicy.CLASS)
@IntDef({AssetPackStorageMethod.STORAGE_FILES, AssetPackStorageMethod.APK_ASSETS})
public @interface AssetPackStorageMethod {
/**
* The asset pack is extracted into a folder containing individual asset files.
* <p>
* Assets contained by this asset pack can be accessed via standard File APIs.
*/
int STORAGE_FILES = 0;
/**
* The asset pack is installed as APKs containing asset files.
* <p>
* Assets contained by this asset pack can be accessed via Asset Manager.
*/
int APK_ASSETS = 1;
}

View file

@ -0,0 +1,22 @@
/*
* SPDX-FileCopyrightText: 2024 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.play.core.assetpacks.model;
import androidx.annotation.IntDef;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.TYPE_USE})
@Retention(RetentionPolicy.CLASS)
@IntDef({AssetPackUpdateAvailability.UNKNOWN, AssetPackUpdateAvailability.UPDATE_NOT_AVAILABLE, AssetPackUpdateAvailability.UPDATE_AVAILABLE})
public @interface AssetPackUpdateAvailability {
int UNKNOWN = 0;
int UPDATE_NOT_AVAILABLE = 1;
int UPDATE_AVAILABLE = 2;
}

View file

@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: 2024 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.play.core.assetpacks.protocol;
import org.microg.gms.common.Hide;
@Hide
public class BroadcastConstants {
public static String ACTION_SESSION_UPDATE = "com.google.android.play.core.assetpacks.receiver.ACTION_SESSION_UPDATE";
public static String EXTRA_SESSION_STATE = "com.google.android.play.core.assetpacks.receiver.EXTRA_SESSION_STATE";
public static String EXTRA_FLAGS = "com.google.android.play.core.FLAGS";
public static String KEY_USING_EXTRACTOR_STREAM = "usingExtractorStream";
}

View file

@ -0,0 +1,448 @@
/*
* SPDX-FileCopyrightText: 2024 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.play.core.assetpacks.protocol;
import android.content.Intent;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.os.BundleCompat;
import org.microg.gms.common.Hide;
import java.util.ArrayList;
@Hide
public final class BundleKeys {
public static RootKey<Integer> APP_VERSION_CODE = new RootKey.Int("app_version_code");
public static RootKey<Integer> CHUNK_NUMBER = new RootKey.Int("chunk_number");
public static RootKey<ParcelFileDescriptor> CHUNK_FILE_DESCRIPTOR = new RootKey.Parcelable<>("chunk_file_descriptor", ParcelFileDescriptor.class);
public static RootKey<Boolean> KEEP_ALIVE = new RootKey.Bool("keep_alive");
public static RootKey<String> MODULE_NAME = new RootKey.String("module_name");
public static RootKey<String> SLICE_ID = new RootKey.String("slice_id");
public static RootKey<ArrayList<String>> PACK_NAMES = new RootKey.StringArrayList("pack_names");
// OptionsBundle
public static RootKey<Integer> PLAY_CORE_VERSION_CODE = new RootKey.Int("playcore_version_code");
public static RootKey<ArrayList<@CompressionFormat Integer>> SUPPORTED_COMPRESSION_FORMATS = new RootKey.IntArrayList("supported_compression_formats");
public static RootKey<ArrayList<@PatchFormat Integer>> SUPPORTED_PATCH_FORMATS = new RootKey.IntArrayList("supported_patch_formats");
// InstalledAssetModulesBundle
public static RootKey<ArrayList<Bundle>> INSTALLED_ASSET_MODULE = new RootKey.ParcelableArrayList<>("installed_asset_module", Bundle.class);
public static RootKey<String> INSTALLED_ASSET_MODULE_NAME = new RootKey.String("installed_asset_module_name");
public static RootKey<Long> INSTALLED_ASSET_MODULE_VERSION = new RootKey.Long("installed_asset_module_version");
public static RootAndPackKey<Integer> SESSION_ID = new RootAndPackKey.Int("session_id");
public static RootAndPackKey<Integer> STATUS = new RootAndPackKey.Int("status");
public static RootAndPackKey<Integer> ERROR_CODE = new RootAndPackKey.Int("error_code");
public static RootAndPackKey<Long> BYTES_DOWNLOADED = new RootAndPackKey.Long("bytes_downloaded");
public static RootAndPackKey<Long> TOTAL_BYTES_TO_DOWNLOAD = new RootAndPackKey.Long("total_bytes_to_download");
public static PackKey<Long> PACK_VERSION = new PackKey.Long("pack_version");
public static PackKey<Long> PACK_BASE_VERSION = new PackKey.Long("pack_base_version");
public static PackKey<String> PACK_VERSION_TAG = new PackKey.String("pack_version_tag");
public static PackKey<ArrayList<String>> SLICE_IDS = new PackKey.StringArrayList("slice_ids");
public static SliceKey<ArrayList<Intent>> CHUNK_INTENTS = new SliceKey.ParcelableArrayList<>("chunk_intents", Intent.class);
public static SliceKey<@CompressionFormat Integer> COMPRESSION_FORMAT = new SliceKey.Int("compression_format");
public static SliceKey<@PatchFormat Integer> PATCH_FORMAT = new SliceKey.Int("patch_format");
public static SliceKey<String> UNCOMPRESSED_HASH_SHA256 = new SliceKey.String("uncompressed_hash_sha256");
public static SliceKey<Long> UNCOMPRESSED_SIZE = new SliceKey.Long("uncompressed_size");
private BundleKeys() {
}
@Nullable
public static <T> T get(Bundle bundle, @NonNull RootKey<T> key) {
return key.get(bundle, key.baseKey());
}
public static <T> T get(Bundle bundle, @NonNull RootKey<T> key, T def) {
return key.get(bundle, key.baseKey(), def);
}
public static <T> void put(Bundle bundle, @NonNull RootKey<T> key, T value) {
key.put(bundle, key.baseKey(), value);
}
@Nullable
public static <T> T get(Bundle bundle, @NonNull PackKey<T> key, String packName) {
return key.get(bundle, packKey(packName, key.baseKey()));
}
public static <T> T get(Bundle bundle, @NonNull PackKey<T> key, String packName, T def) {
return key.get(bundle, packKey(packName, key.baseKey()), def);
}
public static <T> void put(Bundle bundle, @NonNull PackKey<T> key, String packName, T value) {
key.put(bundle, packKey(packName, key.baseKey()), value);
}
@Nullable
public static <T> T get(Bundle bundle, @NonNull SliceKey<T> key, String packName, String sliceId) {
return key.get(bundle, sliceKey(packName, sliceId, key.baseKey()));
}
public static <T> T get(Bundle bundle, @NonNull SliceKey<T> key, String packName, String sliceId, T def) {
return key.get(bundle, sliceKey(packName, sliceId, key.baseKey()), def);
}
public static <T> void put(Bundle bundle, @NonNull SliceKey<T> key, String packName, String sliceId, T value) {
key.put(bundle, sliceKey(packName, sliceId, key.baseKey()), value);
}
@NonNull
private static String packKey(String packName, String baseKey) {
return baseKey + ":" + packName;
}
@NonNull
private static String sliceKey(String packName, String sliceId, String baseKey) {
return baseKey + ":" + packName + ":" + sliceId;
}
public interface TypedBundleKey<T> {
@NonNull
java.lang.String baseKey();
@Nullable
T get(@NonNull Bundle bundle, @NonNull java.lang.String key);
T get(@NonNull Bundle bundle, @NonNull java.lang.String key, T def);
void put(@NonNull Bundle bundle, @NonNull java.lang.String key, T value);
abstract class Base<T> implements TypedBundleKey<T> {
@NonNull
public final java.lang.String baseKey;
public Base(@NonNull java.lang.String baseKey) {
this.baseKey = baseKey;
}
@NonNull
@Override
public java.lang.String baseKey() {
return baseKey;
}
}
class Int extends Base<Integer> {
public Int(@NonNull java.lang.String key) {
super(key);
}
@Override
public Integer get(@NonNull Bundle bundle, @NonNull java.lang.String key) {
return bundle.getInt(key);
}
@Override
public Integer get(@NonNull Bundle bundle, @NonNull java.lang.String key, Integer def) {
return bundle.getInt(key, def);
}
@Override
public void put(@NonNull Bundle bundle, @NonNull java.lang.String key, Integer value) {
bundle.putInt(key, value);
}
}
class Long extends Base<java.lang.Long> {
public Long(@NonNull java.lang.String key) {
super(key);
}
@Override
public java.lang.Long get(@NonNull Bundle bundle, @NonNull java.lang.String key) {
return bundle.getLong(key);
}
@Override
public java.lang.Long get(@NonNull Bundle bundle, @NonNull java.lang.String key, java.lang.Long def) {
return bundle.getLong(key, def);
}
@Override
public void put(@NonNull Bundle bundle, @NonNull java.lang.String key, java.lang.Long value) {
bundle.putLong(key, value);
}
}
class Bool extends Base<Boolean> {
public Bool(@NonNull java.lang.String key) {
super(key);
}
@Override
public Boolean get(@NonNull Bundle bundle, @NonNull java.lang.String key) {
return bundle.getBoolean(key);
}
@Override
public Boolean get(@NonNull Bundle bundle, @NonNull java.lang.String key, Boolean def) {
return bundle.getBoolean(key, def);
}
@Override
public void put(@NonNull Bundle bundle, @NonNull java.lang.String key, Boolean value) {
bundle.putBoolean(key, value);
}
}
class String extends Base<java.lang.String> {
public String(@NonNull java.lang.String key) {
super(key);
}
@Override
public java.lang.String get(@NonNull Bundle bundle, @NonNull java.lang.String key) {
return bundle.getString(key);
}
@Override
public java.lang.String get(@NonNull Bundle bundle, @NonNull java.lang.String key, java.lang.String def) {
return bundle.getString(key, def);
}
@Override
public void put(@NonNull Bundle bundle, @NonNull java.lang.String key, java.lang.String value) {
bundle.putString(key, value);
}
}
class Parcelable<T extends android.os.Parcelable> extends Base<T> {
@NonNull
private final Class<T> tClass;
public Parcelable(@NonNull java.lang.String key, @NonNull Class<T> tClass) {
super(key);
this.tClass = tClass;
}
@Override
public T get(@NonNull Bundle bundle, @NonNull java.lang.String key) {
return BundleCompat.getParcelable(bundle, key, tClass);
}
@Override
public T get(@NonNull Bundle bundle, @NonNull java.lang.String key, T def) {
if (bundle.containsKey(key)) {
return BundleCompat.getParcelable(bundle, key, tClass);
} else {
return def;
}
}
@Override
public void put(@NonNull Bundle bundle, @NonNull java.lang.String key, T value) {
bundle.putParcelable(key, value);
}
}
class StringArrayList extends Base<ArrayList<java.lang.String>> {
public StringArrayList(@NonNull java.lang.String key) {
super(key);
}
@Override
public ArrayList<java.lang.String> get(@NonNull Bundle bundle, @NonNull java.lang.String key) {
return bundle.getStringArrayList(key);
}
@Override
public ArrayList<java.lang.String> get(@NonNull Bundle bundle, @NonNull java.lang.String key, ArrayList<java.lang.String> def) {
if (bundle.containsKey(key)) {
return bundle.getStringArrayList(key);
} else {
return def;
}
}
@Override
public void put(@NonNull Bundle bundle, @NonNull java.lang.String key, ArrayList<java.lang.String> value) {
bundle.putStringArrayList(key, value);
}
}
class IntArrayList extends Base<ArrayList<Integer>> {
public IntArrayList(@NonNull java.lang.String key) {
super(key);
}
@Override
public ArrayList<Integer> get(@NonNull Bundle bundle, @NonNull java.lang.String key) {
return bundle.getIntegerArrayList(key);
}
@Override
public ArrayList<Integer> get(@NonNull Bundle bundle, @NonNull java.lang.String key, ArrayList<Integer> def) {
if (bundle.containsKey(key)) {
return bundle.getIntegerArrayList(key);
} else {
return def;
}
}
@Override
public void put(@NonNull Bundle bundle, @NonNull java.lang.String key, ArrayList<Integer> value) {
bundle.putIntegerArrayList(key, value);
}
}
class ParcelableArrayList<T extends android.os.Parcelable> extends Base<ArrayList<T>> {
@NonNull
private final Class<T> tClass;
public ParcelableArrayList(@NonNull java.lang.String key, @NonNull Class<T> tClass) {
super(key);
this.tClass = tClass;
}
@Override
public ArrayList<T> get(@NonNull Bundle bundle, @NonNull java.lang.String key) {
return BundleCompat.getParcelableArrayList(bundle, key, tClass);
}
@Override
public ArrayList<T> get(@NonNull Bundle bundle, @NonNull java.lang.String key, ArrayList<T> def) {
if (bundle.containsKey(key)) {
return BundleCompat.getParcelableArrayList(bundle, key, tClass);
} else {
return def;
}
}
@Override
public void put(@NonNull Bundle bundle, @NonNull java.lang.String key, ArrayList<T> value) {
bundle.putParcelableArrayList(key, value);
}
}
}
public interface PackKey<T> extends TypedBundleKey<T> {
class Int extends TypedBundleKey.Int implements PackKey<Integer> {
public Int(@NonNull java.lang.String key) {
super(key);
}
}
class Long extends TypedBundleKey.Long implements PackKey<java.lang.Long> {
public Long(@NonNull java.lang.String key) {
super(key);
}
}
class String extends TypedBundleKey.String implements PackKey<java.lang.String> {
public String(@NonNull java.lang.String key) {
super(key);
}
}
class StringArrayList extends TypedBundleKey.StringArrayList implements PackKey<ArrayList<java.lang.String>> {
public StringArrayList(@NonNull java.lang.String key) {
super(key);
}
}
}
public interface SliceKey<T> extends TypedBundleKey<T> {
class Int extends TypedBundleKey.Int implements SliceKey<Integer> {
public Int(@NonNull java.lang.String key) {
super(key);
}
}
class Long extends TypedBundleKey.Long implements SliceKey<java.lang.Long> {
public Long(@NonNull java.lang.String key) {
super(key);
}
}
class String extends TypedBundleKey.String implements SliceKey<java.lang.String> {
public String(@NonNull java.lang.String key) {
super(key);
}
}
class ParcelableArrayList<T extends android.os.Parcelable> extends TypedBundleKey.ParcelableArrayList<T> implements SliceKey<ArrayList<T>> {
public ParcelableArrayList(@NonNull java.lang.String key, @NonNull Class<T> tClass) {
super(key, tClass);
}
}
}
public interface RootKey<T> extends TypedBundleKey<T> {
class Int extends TypedBundleKey.Int implements RootKey<Integer> {
public Int(@NonNull java.lang.String key) {
super(key);
}
}
class Long extends TypedBundleKey.Long implements RootKey<java.lang.Long> {
public Long(@NonNull java.lang.String key) {
super(key);
}
}
class Bool extends TypedBundleKey.Bool implements RootKey<Boolean> {
public Bool(@NonNull java.lang.String key) {
super(key);
}
}
class String extends TypedBundleKey.String implements RootKey<java.lang.String> {
public String(@NonNull java.lang.String key) {
super(key);
}
}
class Parcelable<T extends android.os.Parcelable> extends TypedBundleKey.Parcelable<T> implements RootKey<T> {
public Parcelable(@NonNull java.lang.String key, @NonNull Class<T> tClass) {
super(key, tClass);
}
}
class StringArrayList extends TypedBundleKey.StringArrayList implements RootKey<ArrayList<java.lang.String>> {
public StringArrayList(@NonNull java.lang.String key) {
super(key);
}
}
class IntArrayList extends TypedBundleKey.IntArrayList implements RootKey<ArrayList<Integer>> {
public IntArrayList(@NonNull java.lang.String key) {
super(key);
}
}
class ParcelableArrayList<T extends android.os.Parcelable> extends TypedBundleKey.ParcelableArrayList<T> implements RootKey<ArrayList<T>> {
public ParcelableArrayList(@NonNull java.lang.String key, @NonNull Class<T> tClass) {
super(key, tClass);
}
}
}
public interface RootAndPackKey<T> extends RootKey<T>, PackKey<T> {
class Int extends TypedBundleKey.Int implements RootAndPackKey<Integer> {
public Int(@NonNull java.lang.String key) {
super(key);
}
}
class Long extends TypedBundleKey.Long implements RootAndPackKey<java.lang.Long> {
public Long(@NonNull java.lang.String key) {
super(key);
}
}
}
}

View file

@ -0,0 +1,24 @@
/*
* SPDX-FileCopyrightText: 2024 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.play.core.assetpacks.protocol;
import androidx.annotation.IntDef;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.TYPE_USE})
@Retention(RetentionPolicy.CLASS)
@IntDef({CompressionFormat.UNSPECIFIED, CompressionFormat.BROTLI, CompressionFormat.GZIP, CompressionFormat.CHUNKED_GZIP, CompressionFormat.CHUNKED_BROTLI})
public @interface CompressionFormat {
int UNSPECIFIED = 0;
int BROTLI = 1;
int GZIP = 2;
int CHUNKED_GZIP = 3;
int CHUNKED_BROTLI = 4;
}

View file

@ -0,0 +1,31 @@
/*
* SPDX-FileCopyrightText: 2024 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.play.core.assetpacks.protocol;
import androidx.annotation.IntDef;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.TYPE_USE})
@Retention(RetentionPolicy.CLASS)
@IntDef({PatchFormat.UNKNOWN_PATCHING_FORMAT, PatchFormat.PATCH_GDIFF, PatchFormat.GZIPPED_GDIFF, PatchFormat.GZIPPED_BSDIFF, PatchFormat.GZIPPED_FILEBYFILE, PatchFormat.BROTLI_FILEBYFILE, PatchFormat.BROTLI_BSDIFF, PatchFormat.BROTLI_FILEBYFILE_RECURSIVE, PatchFormat.BROTLI_FILEBYFILE_ANDROID_AWARE, PatchFormat.BROTLI_FILEBYFILE_RECURSIVE_ANDROID_AWARE, PatchFormat.BROTLI_FILEBYFILE_ANDROID_AWARE_NO_RECOMPRESSION})
public @interface PatchFormat {
int UNKNOWN_PATCHING_FORMAT = 0;
int PATCH_GDIFF = 1;
int GZIPPED_GDIFF = 2;
int GZIPPED_BSDIFF = 3;
int GZIPPED_FILEBYFILE = 4;
int BROTLI_FILEBYFILE = 5;
int BROTLI_BSDIFF = 6;
int BROTLI_FILEBYFILE_RECURSIVE = 7;
int BROTLI_FILEBYFILE_ANDROID_AWARE = 8;
int BROTLI_FILEBYFILE_RECURSIVE_ANDROID_AWARE = 9;
int BROTLI_FILEBYFILE_ANDROID_AWARE_NO_RECOMPRESSION = 10;
}

View file

@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: 2024 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.play.core.listener;
/**
* Base interface for state update listeners.
*/
public interface StateUpdateListener<StateT> {
/**
* Callback triggered whenever the state has changed.
*/
void onStateUpdate(StateT state);
}

View file

@ -0,0 +1,84 @@
/*
* SPDX-FileCopyrightText: 2024 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.vending;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.ResolveInfo;
import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public class MarketIntentRedirect extends Activity {
private static final String TAG = "IntentForwarder";
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = getIntent();
if (intent != null) {
try {
processIntent(intent);
} catch (Exception e) {
Log.w(TAG, "Failed forwarding", e);
}
} else {
Log.w(TAG, "Intent is null, ignoring");
}
finish();
}
private boolean isNonSelfIntent(@NonNull Intent intent) {
ResolveInfo resolveInfo = getPackageManager().resolveActivity(intent, 0);
return resolveInfo != null && resolveInfo.activityInfo != null && !getPackageName().equals(resolveInfo.activityInfo.packageName);
}
private void processIntent(@NonNull Intent intent) {
Log.d(TAG, "Received " + intent);
Intent newIntent = new Intent(intent);
newIntent.setPackage(null);
newIntent.setComponent(null);
if ("market".equals(newIntent.getScheme())) {
try {
if (isNonSelfIntent(newIntent)) {
Log.d(TAG, "Redirect to " + newIntent);
startActivity(newIntent);
return;
}
} catch (Exception e) {
Log.w(TAG, e);
}
// Rewrite to market.android.com as there is no handler for market://
// This allows to always still open in a web browser
newIntent.setData(newIntent.getData().buildUpon()
.scheme("https").authority("market.android.com")
.encodedPath(newIntent.getData().getAuthority() + newIntent.getData().getEncodedPath())
.build());
Log.d(TAG, "Rewrote as " + newIntent + " (" + newIntent.getDataString() + ")");
}
if ("market.android.com".equals(newIntent.getData().getAuthority()) && newIntent.getData().getPath().startsWith("/details")) {
// Rewrite to play.google.com for better compatibility
newIntent.setData(newIntent.getData().buildUpon()
.scheme("https").authority("play.google.com")
.encodedPath("/store/apps" + newIntent.getData().getEncodedPath())
.build());
Log.d(TAG, "Rewrote as " + newIntent + " (" + newIntent.getDataString() + ")");
}
try {
if (isNonSelfIntent(newIntent)) {
Log.d(TAG, "Redirect to " + newIntent);
startActivity(newIntent);
return;
}
} catch (Exception e) {
Log.w(TAG, e);
Toast.makeText(this, "Unable to open", Toast.LENGTH_SHORT).show();
}
Log.w(TAG, "Not forwarded " + intent);
}
}

View file

@ -0,0 +1,43 @@
/*
* SPDX-FileCopyrightText: 2025 e foundation
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.vending
import android.accounts.AccountManager
import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.util.Log
import org.microg.gms.auth.AuthConstants
import org.microg.vending.ui.WorkAppsActivity
class WorkAccountChangedReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent?) {
val accountManager = AccountManager.get(context)
val hasWorkAccounts = accountManager.getAccountsByType(AuthConstants.WORK_ACCOUNT_TYPE).isNotEmpty()
if (android.os.Build.VERSION.SDK_INT >= 21) {
Log.d(TAG, "setting VendingActivity state to enabled = $hasWorkAccounts")
val componentName = ComponentName(
context,
WorkAppsActivity::class.java
)
context.packageManager.setComponentEnabledSetting(
componentName,
if (hasWorkAccounts) PackageManager.COMPONENT_ENABLED_STATE_ENABLED else PackageManager.COMPONENT_ENABLED_STATE_DEFAULT,
0
)
}
}
companion object {
const val TAG = "GmsVendingWorkAccRcvr"
}
}

View file

@ -0,0 +1,83 @@
package org.microg.vending.billing
import android.accounts.Account
import android.content.Context
import android.util.Log
import io.ktor.utils.io.errors.IOException
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.GoogleApiResponse
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 = GoogleApiResponse.ADAPTER
).payload?.detailsResponse
} catch (e: IOException) {
Log.e(TAG, "Unable to auto-purchase $packageName because of a network error or unexpected response when gathering app data", e)
return false
}
val item = detailsResult?.item
val appDetails = item?.details?.appDetails
val versionCode = appDetails?.versionCode
if (detailsResult == null || versionCode == null || appDetails.packageName != packageName) {
Log.e(TAG, "Unable to auto-purchase $packageName because the server did not send sufficient or matching 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 = 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
}
if (buyResult?.deliveryToken.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
}

View file

@ -0,0 +1,38 @@
/*
* SPDX-FileCopyrightText: 2024 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.vending.billing
import android.accounts.Account
import android.accounts.AccountManager
import android.content.Context
import android.content.Intent
import android.util.Log
import org.microg.vending.billing.core.AuthData
import java.util.concurrent.TimeUnit
object AuthManager {
private const val TOKEN_TYPE = "oauth2:https://www.googleapis.com/auth/googleplay https://www.googleapis.com/auth/accounts.reauth"
fun getAuthData(context: Context, account: Account? = AccountManager.get(context).getAccountsByType(DEFAULT_ACCOUNT_TYPE).firstOrNull()): AuthData? {
if (account == null) return null
val deviceCheckInConsistencyToken = CheckinServiceClient.getConsistencyToken(context)
val gsfId = GServices.getString(context.contentResolver, "android_id", "0")!!.toBigInteger().toString(16)
if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "gsfId: $gsfId, deviceDataVersionInfo: $deviceCheckInConsistencyToken")
val accountManager: AccountManager = AccountManager.get(context)
val future = accountManager.getAuthToken(account, TOKEN_TYPE, false, null, null)
val bundle = future.getResult(15, TimeUnit.SECONDS)
val launch = bundle.getParcelable(AccountManager.KEY_INTENT) as Intent?
return if (launch != null) {
Log.e(TAG, "[getAuthData]need start activity by intent: $launch")
null
} else {
bundle.getString(AccountManager.KEY_AUTHTOKEN)?.let {
AuthData(account.name, it, gsfId, deviceCheckInConsistencyToken)
}
}
}
}

View file

@ -0,0 +1,49 @@
/*
* SPDX-FileCopyrightText: 2024 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.vending.billing
import android.content.ComponentName
import android.content.Context
import android.content.Context.BIND_AUTO_CREATE
import android.content.Intent
import android.content.ServiceConnection
import android.os.IBinder
import android.util.Log
import com.google.android.gms.checkin.internal.ICheckinService
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.TimeUnit
// TODO: Connect to check-in settings provider instead
object CheckinServiceClient {
private val serviceQueue = LinkedBlockingQueue<ICheckinService>()
fun getConsistencyToken(context: Context): String {
try {
val conn = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
service?.let { serviceQueue.add(ICheckinService.Stub.asInterface(it)) }
}
override fun onServiceDisconnected(name: ComponentName?) {
serviceQueue.clear()
}
}
val intent = Intent("com.google.android.gms.checkin.BIND_TO_SERVICE")
intent.setPackage("com.google.android.gms")
val res = context.bindService(intent, conn, BIND_AUTO_CREATE)
if (!res) return ""
try {
val service = serviceQueue.poll(10, TimeUnit.SECONDS) ?: return ""
return service.deviceDataVersionInfo ?: ""
} finally {
context.unbindService(conn)
}
} catch (e: Exception) {
Log.e(TAG, "getConsistencyToken", e)
return ""
}
}
}

View file

@ -0,0 +1,17 @@
/*
* SPDX-FileCopyrightText: 2024 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.vending.billing
const val TAG = "Billing"
// TODO: What versions to use?
const val VENDING_VERSION_CODE = 83061810L
const val VENDING_VERSION_NAME = "30.6.18-21 [0] [PR] 450795914"
const val VENDING_PACKAGE_NAME = "com.android.vending"
// TODO: Replace key name
const val KEY_IAP_SHEET_UI_PARAM = "key_iap_sheet_ui_param"
const val DEFAULT_ACCOUNT_TYPE = "com.google"
const val ADD_PAYMENT_METHOD_URL = "https://play.google.com/store/paymentmethods"

View file

@ -0,0 +1,19 @@
/*
* SPDX-FileCopyrightText: 2024 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.vending.billing
import android.app.Application
import org.microg.gms.profile.ProfileManager
// TODO: Get rid
object ContextProvider {
lateinit var context: Application
private set
fun init(application: Application) {
context = application
}
}

View file

@ -0,0 +1,71 @@
/*
* SPDX-FileCopyrightText: 2024 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.vending.billing
import java.util.Random
// TODO: Use existing code
object DeviceIdentifier {
var wifiMac = randomMacAddress()
var meid = randomMeid()
var serial = randomSerial("008741A0B2C4D6E8")
var esn: String = ""
private fun randomMacAddress(): String {
var mac = "b407f9"
val rand = Random()
for (i in 0..5) {
mac += rand.nextInt(16).toString(16)
}
return mac
}
private fun randomSerial(
template: String,
prefixLength: Int = (template.length / 2).coerceAtMost(6)
): String {
val serial = StringBuilder()
template.forEachIndexed { index, c ->
serial.append(
when {
index < prefixLength -> c
c.isDigit() -> '0' + kotlin.random.Random.nextInt(10)
c.isLowerCase() && c <= 'f' -> 'a' + kotlin.random.Random.nextInt(6)
c.isLowerCase() -> 'a' + kotlin.random.Random.nextInt(26)
c.isUpperCase() && c <= 'F' -> 'A' + kotlin.random.Random.nextInt(6)
c.isUpperCase() -> 'A' + kotlin.random.Random.nextInt(26)
else -> c
}
)
}
return serial.toString()
}
private fun randomMeid(): String {
// http://en.wikipedia.org/wiki/International_Mobile_Equipment_Identity
// We start with a known base, and generate random MEID
var meid = "35503104"
val rand = Random()
for (i in 0..5) {
meid += rand.nextInt(10).toString()
}
// Luhn algorithm (check digit)
var sum = 0
for (i in meid.indices) {
var c = meid[i].toString().toInt()
if ((meid.length - i - 1) % 2 == 0) {
c *= 2
c = c % 10 + c / 10
}
sum += c
}
val check = (100 - sum) % 10
meid += check.toString()
return meid
}
}

View file

@ -0,0 +1,20 @@
package org.microg.vending.billing
import android.content.ContentResolver
import android.net.Uri
// TODO: Move
object GServices {
private val CONTENT_URI: Uri = Uri.parse("content://com.google.android.gsf.gservices")
fun getString(resolver: ContentResolver, key: String, defaultValue: String?): String? {
var result = defaultValue
val cursor = resolver.query(CONTENT_URI, null, null, arrayOf(key), null)
cursor?.use {
if (cursor.moveToNext()) {
result = cursor.getString(1)
}
}
return result
}
}

View file

@ -0,0 +1,710 @@
/*
* SPDX-FileCopyrightText: 2024 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.vending.billing
import android.accounts.Account
import android.app.PendingIntent.FLAG_CANCEL_CURRENT
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.app.PendingIntentCompat
import androidx.core.os.bundleOf
import com.android.billingclient.api.BillingClient.BillingResponseCode
import com.android.billingclient.api.BillingClient.ProductType
import com.android.vending.VendingPreferences
import com.android.vending.billing.IInAppBillingCreateAlternativeBillingOnlyTokenCallback
import com.android.vending.billing.IInAppBillingCreateExternalPaymentReportingDetailsCallback
import com.android.vending.billing.IInAppBillingDelegateToBackendCallback
import com.android.vending.billing.IInAppBillingGetAlternativeBillingOnlyDialogIntentCallback
import com.android.vending.billing.IInAppBillingGetBillingConfigCallback
import com.android.vending.billing.IInAppBillingGetExternalPaymentDialogIntentCallback
import com.android.vending.billing.IInAppBillingIsAlternativeBillingOnlyAvailableCallback
import com.android.vending.billing.IInAppBillingIsExternalPaymentAvailableCallback
import com.android.vending.billing.IInAppBillingService
import com.android.vending.billing.IInAppBillingServiceCallback
import org.microg.vending.billing.ui.InAppBillingHostActivity
import org.microg.vending.billing.ui.logic.BuyFlowResult
import com.google.android.gms.droidguard.DroidGuardClient
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.runBlocking
import org.json.JSONObject
import org.microg.gms.utils.toHexString
import org.microg.vending.billing.core.*
import java.util.Locale
private class BuyFlowCacheEntry(
var packageName: String,
var account: Account,
var buyFlowParams: BuyFlowParams? = null,
var lastAcquireResult: AcquireResult? = null,
var droidGuardResult: String = ""
)
private const val EXPIRE_MS = 1 * 60 * 1000
private data class IAPCoreCacheEntry(
val iapCore: IAPCore,
val expiredAt: Long
)
private const val requestCode = 10001
@RequiresApi(21)
class InAppBillingServiceImpl(private val context: Context) : IInAppBillingService.Stub() {
companion object {
private val buyFlowCacheMap = mutableMapOf<String, BuyFlowCacheEntry>()
private val iapCoreCacheMap = mutableMapOf<String, IAPCoreCacheEntry>()
private val typeList = listOf(
ProductType.SUBS,
ProductType.INAPP,
"first_party",
"audio_book",
"book",
"book_subs",
"nest_subs",
"play_pass_subs",
"stadia_item",
"stadia_subs",
"movie",
"tv_show",
"tv_episode",
"tv_season"
)
fun acquireRequest(
context: Context,
cacheKey: String,
actionContexts: List<ByteArray> = emptyList(),
authToken: String? = null,
firstRequest: Boolean = false
): BuyFlowResult {
if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "acquireRequest(cacheKey=$cacheKey, actionContexts=${actionContexts.map { it.toHexString() }}, authToken=$authToken)")
val buyFlowCacheEntry = buyFlowCacheMap[cacheKey] ?: return BuyFlowResult(
null, null, resultBundle(BillingResponseCode.DEVELOPER_ERROR, "Parameter check error.")
)
val buyFlowParams = buyFlowCacheEntry.buyFlowParams ?: return BuyFlowResult(
null, buyFlowCacheEntry.account, resultBundle(BillingResponseCode.DEVELOPER_ERROR, "Parameter check error.")
)
val params = AcquireParams(
buyFlowParams = buyFlowParams,
actionContext = actionContexts,
authToken = authToken,
droidGuardResult = buyFlowCacheEntry.droidGuardResult.takeIf { !firstRequest },
lastAcquireResult = buyFlowCacheEntry.lastAcquireResult.takeIf { !firstRequest }
)
val coreResult = try {
val deferred = CoroutineScope(Dispatchers.IO).async {
createIAPCore(
context,
buyFlowCacheEntry.account,
buyFlowCacheEntry.packageName
).doAcquireRequest(
params
)
}
runBlocking { deferred.await() }
} catch (e: RuntimeException) {
if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "acquireRequest", e)
return BuyFlowResult(null, buyFlowCacheEntry.account, resultBundle(BillingResponseCode.DEVELOPER_ERROR, e.message))
} catch (e: Exception) {
if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "acquireRequest", e)
return BuyFlowResult(null, buyFlowCacheEntry.account, resultBundle(BillingResponseCode.DEVELOPER_ERROR, "Internal error."))
}
if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "acquireRequest acquireParsedResult: ${coreResult.acquireParsedResult}")
buyFlowCacheEntry.lastAcquireResult = coreResult
if (coreResult.acquireParsedResult.action?.droidGuardMap?.isNotEmpty() == true) {
DroidGuardClient.getResults(context, "phonesky_acquire_flow", coreResult.acquireParsedResult.action.droidGuardMap).addOnCompleteListener { task ->
buyFlowCacheEntry.droidGuardResult = task.result
}
}
coreResult.acquireParsedResult.purchaseItems.forEach {
PurchaseManager.addPurchase(buyFlowCacheEntry.account, buyFlowCacheEntry.packageName, it)
}
return BuyFlowResult(
coreResult.acquireParsedResult,
buyFlowCacheEntry.account,
coreResult.acquireParsedResult.result.toBundle()
)
}
fun requestAuthProofToken(context: Context, cacheKey: String, password: String): String {
val buyFlowCacheEntry = buyFlowCacheMap[cacheKey]
?: throw IllegalStateException("Nothing cached: $cacheKey")
val deferred = CoroutineScope(Dispatchers.IO).async {
createIAPCore(
context,
buyFlowCacheEntry.account,
buyFlowCacheEntry.packageName
).requestAuthProofToken(password)
}
return runBlocking { deferred.await() }
}
private fun createIAPCore(context: Context, account: Account, pkgName: String): IAPCore {
val key = "$pkgName:${account.name}"
val cacheEntry = iapCoreCacheMap[key]
if (cacheEntry != null) {
if (cacheEntry.expiredAt > System.currentTimeMillis())
return cacheEntry.iapCore
iapCoreCacheMap.remove(key)
}
val authData = AuthManager.getAuthData(context, account)
?: throw RuntimeException("Failed to obtain login token.")
val deviceEnvInfo = createDeviceEnvInfo(context)
?: throw RuntimeException("Failed to retrieve device information.")
val clientInfo = createClient(context, pkgName)
?: throw RuntimeException("Failed to retrieve client information.")
val iapCore = IAPCore(context.applicationContext, deviceEnvInfo, clientInfo, authData)
iapCoreCacheMap[key] =
IAPCoreCacheEntry(iapCore, System.currentTimeMillis() + EXPIRE_MS)
return iapCore
}
}
private fun getPreferredAccount(extraParams: Bundle?): Account {
val name = extraParams?.getString("accountName")
name?.let {
extraParams.remove("accountName")
}
return getGoogleAccount(context, name)
?: throw RuntimeException("No Google account found.")
}
private fun isBillingSupported(
apiVersion: Int,
type: String?,
packageName: String,
extraParams: Bundle?
): Bundle {
if (!VendingPreferences.isBillingEnabled(context)) {
Log.w(TAG, "isBillingSupported: Billing is disabled")
return resultBundle(BillingResponseCode.BILLING_UNAVAILABLE, "Billing is disabled")
}
if (apiVersion < 3) {
return resultBundle(BillingResponseCode.BILLING_UNAVAILABLE, "Client does not support the requesting billing API.")
}
if (extraParams != null && apiVersion < 7) {
return resultBundle(BillingResponseCode.DEVELOPER_ERROR, "ExtraParams was introduced in API version 7.")
}
if (type.isNullOrBlank()) {
return resultBundle(BillingResponseCode.DEVELOPER_ERROR, "SKU type can't be empty.")
}
if (!typeList.contains(type)) {
return resultBundle(BillingResponseCode.DEVELOPER_ERROR, "Invalid SKU type: $type")
}
if (extraParams != null && !extraParams.isEmpty && extraParams.getBoolean("vr") && type == "subs") {
return resultBundle(BillingResponseCode.BILLING_UNAVAILABLE, "subscription is not supported in VR Mode.")
}
return resultBundle(BillingResponseCode.OK, "")
}
override fun isBillingSupported(apiVersion: Int, packageName: String?, type: String?): Int {
val result = isBillingSupported(apiVersion, type, packageName!!, null)
Log.d(TAG, "isBillingSupported(apiVersion=$apiVersion, packageName=$packageName, type=$type)=$result")
return result.getInt("RESPONSE_CODE")
}
override fun getSkuDetails(
apiVersion: Int,
packageName: String?,
type: String?,
skusBundle: Bundle?
): Bundle {
Log.d(TAG, "getSkuDetails(apiVersion=$apiVersion, packageName=$packageName, type=$type, skusBundle=$skusBundle)")
return resultBundle(BillingResponseCode.BILLING_UNAVAILABLE, "Not yet implemented")
}
override fun getBuyIntent(
apiVersion: Int,
packageName: String?,
sku: String?,
type: String?,
developerPayload: String?
): Bundle {
if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "getBuyIntent(apiVersion=$apiVersion, packageName=$packageName, sku=$sku, type=$type, developerPayload=$developerPayload)")
return runCatching { getBuyIntentExtraParams(apiVersion, packageName!!, sku!!, type!!, developerPayload, null) }
.getOrDefault(resultBundle(BillingResponseCode.BILLING_UNAVAILABLE, "Not yet implemented"))
}
override fun getPurchases(
apiVersion: Int,
packageName: String?,
type: String?,
continuationToken: String?
): Bundle {
if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "getPurchases(apiVersion=$apiVersion, packageName=$packageName, type=$type, continuationToken=$continuationToken)")
return resultBundle(BillingResponseCode.BILLING_UNAVAILABLE, "Not yet implemented")
}
override fun consumePurchase(
apiVersion: Int,
packageName: String?,
purchaseToken: String?
): Int {
Log.d(TAG, "consumePurchase(apiVersion=$apiVersion, packageName=$packageName, purchaseToken=$purchaseToken)")
return BillingResponseCode.BILLING_UNAVAILABLE
}
override fun isPromoEligible(apiVersion: Int, packageName: String?, type: String?): Int {
Log.d(TAG, "isPromoEligible(apiVersion=$apiVersion, packageName=$packageName, type=$type)")
return BillingResponseCode.BILLING_UNAVAILABLE
}
override fun getBuyIntentToReplaceSkus(
apiVersion: Int,
packageName: String?,
oldSkus: MutableList<String>?,
newSku: String?,
type: String?,
developerPayload: String?
): Bundle {
Log.d(TAG, "getBuyIntentToReplaceSkus(apiVersion=$apiVersion, packageName=$packageName, oldSkus=$oldSkus, newSku=$newSku, type=$type, developerPayload=$developerPayload)")
return resultBundle(BillingResponseCode.BILLING_UNAVAILABLE, "Not yet implemented")
}
override fun getBuyIntentExtraParams(
apiVersion: Int,
packageName: String,
sku: String,
type: String,
developerPayload: String?,
extraParams: Bundle?
): Bundle {
if (!VendingPreferences.isBillingEnabled(context)) {
Log.w(TAG, "getBuyIntentExtraParams: Billing is disabled")
return resultBundle(BillingResponseCode.BILLING_UNAVAILABLE, "Billing is disabled")
}
extraParams?.size()
Log.d(TAG, "getBuyIntentExtraParams(apiVersion=$apiVersion, packageName=$packageName, sku=$sku, type=$type, developerPayload=$developerPayload, extraParams=$extraParams)")
val skuSerializedDocIdList =
extraParams?.getStringArrayList("SKU_SERIALIZED_DOCID_LIST")
skuSerializedDocIdList?.forEach {
if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "serializedDocId=$it")
}
val skuOfferTypeList = extraParams?.getIntegerArrayList("SKU_OFFER_TYPE_LIST")
skuOfferTypeList?.forEach {
if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "skuOfferType=$it")
}
val skuOfferIdTokenList = extraParams?.getStringArrayList("SKU_OFFER_ID_TOKEN_LIST")
skuOfferIdTokenList?.forEach {
if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "skuOfferIdToken=$it")
}
val accountName = extraParams?.getString("accountName")?.also {
if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "accountName=$it")
}
val oldSkuPurchaseToken = extraParams?.getString("oldSkuPurchaseToken")?.also {
if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "oldSkuPurchaseToken=$it")
}
val oldSkuPurchaseId = extraParams?.getString("oldSkuPurchaseId")?.also {
if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "oldSkuPurchaseId=$it")
}
extraParams?.let {
it.remove("skusToReplace")
it.remove("oldSkuPurchaseToken")
it.remove("vr")
it.remove("isDynamicSku")
it.remove("rewardToken")
it.remove("childDirected")
it.remove("underAgeOfConsent")
it.remove("additionalSkus")
it.remove("additionalSkuTypes")
it.remove("SKU_OFFER_ID_TOKEN_LIST")
it.remove("SKU_OFFER_ID_LIST")
it.remove("SKU_OFFER_TYPE_LIST")
it.remove("SKU_SERIALIZED_DOCID_LIST")
it.remove("oldSkuPurchaseId")
}
val account = try {
getPreferredAccount(extraParams)
} catch (e: RuntimeException) {
return resultBundle(BillingResponseCode.BILLING_UNAVAILABLE, e.message)
}
val params = BuyFlowParams(
apiVersion = apiVersion,
sku = sku,
skuType = type,
developerPayload = developerPayload ?: "",
skuParams = bundleToMap(extraParams),
needAuth = SettingsManager(context).getAuthStatus(),
skuSerializedDockIdList = skuSerializedDocIdList,
skuOfferIdTokenList = skuOfferIdTokenList,
oldSkuPurchaseId = oldSkuPurchaseId,
oldSkuPurchaseToken = oldSkuPurchaseToken
)
val cacheEntryKey = "${packageName}:${account.name}"
buyFlowCacheMap[cacheEntryKey] =
BuyFlowCacheEntry(packageName, account, buyFlowParams = params)
val intent = Intent(context, InAppBillingHostActivity::class.java)
intent.putExtra(KEY_IAP_SHEET_UI_PARAM, cacheEntryKey)
val buyFlowPendingIntent = PendingIntentCompat.getActivity(context, requestCode, intent, FLAG_CANCEL_CURRENT, false)
return resultBundle(BillingResponseCode.OK, "", bundleOf("BUY_INTENT" to buyFlowPendingIntent))
}
override fun getPurchaseHistory(
apiVersion: Int,
packageName: String?,
type: String?,
continuationToken: String?,
extraParams: Bundle?
): Bundle {
if (!VendingPreferences.isBillingEnabled(context)) {
Log.w(TAG, "getPurchaseHistory: Billing is disabled")
return resultBundle(BillingResponseCode.BILLING_UNAVAILABLE, "Billing is disabled")
}
extraParams?.size()
Log.d(TAG, "getPurchaseHistory(apiVersion=$apiVersion, packageName=$packageName, type=$type, continuationToken=$continuationToken, extraParams=$extraParams)")
val account = try {
getPreferredAccount(extraParams)
} catch (e: RuntimeException) {
return resultBundle(BillingResponseCode.BILLING_UNAVAILABLE, e.message)
}
val params = GetPurchaseHistoryParams(
apiVersion = apiVersion,
type = type!!,
continuationToken = continuationToken,
extraParams = bundleToMap(extraParams)
)
val coreResult = try {
val deferred = CoroutineScope(Dispatchers.IO).async {
createIAPCore(context, account, packageName!!).getPurchaseHistory(params)
}
runBlocking {
deferred.await()
}
} catch (e: RuntimeException) {
return resultBundle(BillingResponseCode.DEVELOPER_ERROR, e.message)
} catch (e: Exception) {
Log.e(TAG, "getPurchaseHistory", e)
return resultBundle(BillingResponseCode.DEVELOPER_ERROR, "Internal error.")
}
if (coreResult.getCode() == BillingResponseCode.OK) {
val itemList = ArrayList<String>()
val dataList = ArrayList<String>()
val signatureList = ArrayList<String>()
coreResult.purchaseHistoryList?.forEach {
itemList.add(it.sku)
dataList.add(it.jsonData)
signatureList.add(it.signature)
}
val result = Bundle()
result.putStringArrayList("INAPP_PURCHASE_ITEM_LIST", itemList)
result.putStringArrayList("INAPP_PURCHASE_DATA_LIST", dataList)
result.putStringArrayList("INAPP_DATA_SIGNATURE_LIST", signatureList)
if (!coreResult.continuationToken.isNullOrEmpty()) {
result.putString("INAPP_CONTINUATION_TOKEN", coreResult.continuationToken)
}
return resultBundle(BillingResponseCode.OK, "", result)
}
return coreResult.resultMap.toBundle()
}
override fun isBillingSupportedExtraParams(
apiVersion: Int,
packageName: String?,
type: String?,
extraParams: Bundle?
): Int {
extraParams?.size()
val result = isBillingSupported(apiVersion, type, packageName!!, extraParams)
Log.d(TAG, "isBillingSupportedExtraParams(apiVersion=$apiVersion, packageName=$packageName, type=$type, extraParams=$extraParams)=$result")
return result.getInt("RESPONSE_CODE")
}
override fun getPurchasesExtraParams(
apiVersion: Int,
packageName: String?,
type: String?,
continuationToken: String?,
extraParams: Bundle?
): Bundle {
if (!VendingPreferences.isBillingEnabled(context)) {
Log.w(TAG, "getPurchasesExtraParams: Billing is disabled")
return resultBundle(BillingResponseCode.BILLING_UNAVAILABLE, "Billing is disabled")
}
extraParams?.size()
Log.d(TAG, "getPurchasesExtraParams(apiVersion=$apiVersion, packageName=$packageName, type=$type, continuationToken=$continuationToken, extraParams=$extraParams)")
if (apiVersion < 7 && extraParams != null) {
return resultBundle(BillingResponseCode.DEVELOPER_ERROR, "Parameter check error.")
}
val account = try {
getPreferredAccount(extraParams)
} catch (e: RuntimeException) {
return resultBundle(BillingResponseCode.BILLING_UNAVAILABLE, e.message)
}
val enablePendingPurchases = extraParams?.getBoolean("enablePendingPurchases", false) ?: false
val itemList = ArrayList<String>()
val dataList = ArrayList<String>()
val signatureList = ArrayList<String>()
PurchaseManager.queryPurchases(account, packageName!!, type!!).filter {
if (it.type == "subs" && it.expireAt < System.currentTimeMillis()) return@filter false
true
}.forEach {
if (enablePendingPurchases || it.purchaseState != 4) {
itemList.add(it.sku)
dataList.add(it.jsonData)
signatureList.add(it.signature)
}
}
val result = Bundle()
result.putStringArrayList("INAPP_PURCHASE_ITEM_LIST", itemList)
result.putStringArrayList("INAPP_PURCHASE_DATA_LIST", dataList)
result.putStringArrayList("INAPP_DATA_SIGNATURE_LIST", signatureList)
return resultBundle(BillingResponseCode.OK, "", result)
}
override fun consumePurchaseExtraParams(
apiVersion: Int,
packageName: String?,
purchaseToken: String,
extraParams: Bundle?
): Bundle {
if (!VendingPreferences.isBillingEnabled(context)) {
Log.w(TAG, "consumePurchaseExtraParams: Billing is disabled")
return resultBundle(BillingResponseCode.BILLING_UNAVAILABLE, "Billing is disabled")
}
extraParams?.size()
Log.d(TAG, "consumePurchaseExtraParams(apiVersion=$apiVersion, packageName=$packageName, purchaseToken=$purchaseToken, extraParams=$extraParams)")
val account = try {
getPreferredAccount(extraParams)
} catch (e: RuntimeException) {
return resultBundle(BillingResponseCode.BILLING_UNAVAILABLE, e.message)
}
val params = ConsumePurchaseParams(
apiVersion = apiVersion,
purchaseToken = purchaseToken,
extraParams = bundleToMap(extraParams)
)
val coreResult = try {
val deferred = CoroutineScope(Dispatchers.IO).async {
val coreResult = createIAPCore(context, account, packageName!!).consumePurchase(params)
if (coreResult.getCode() == BillingResponseCode.OK) {
PurchaseManager.removePurchase(purchaseToken)
}
coreResult
}
runBlocking { deferred.await() }
} catch (e: RuntimeException) {
return resultBundle(BillingResponseCode.DEVELOPER_ERROR, e.message)
} catch (e: Exception) {
Log.e(TAG, "consumePurchaseExtraParams", e)
return resultBundle(BillingResponseCode.DEVELOPER_ERROR, "Internal error.")
}
return coreResult.resultMap.toBundle()
}
override fun getPriceChangeConfirmationIntent(
apiVersion: Int,
packageName: String?,
sku: String?,
type: String?,
extraParams: Bundle?
): Bundle {
extraParams?.size()
Log.d(TAG, "getPriceChangeConfirmationIntent(apiVersion=$apiVersion, packageName=$packageName, sku=$sku, type=$type, extraParams=$extraParams)")
return resultBundle(BillingResponseCode.BILLING_UNAVAILABLE, "Not yet implemented")
}
override fun getSkuDetailsExtraParams(
apiVersion: Int,
packageName: String?,
type: String?,
skuBundle: Bundle?,
extraParams: Bundle?
): Bundle {
if (!VendingPreferences.isBillingEnabled(context)) {
Log.w(TAG, "getSkuDetailsExtraParams: Billing is disabled")
return resultBundle(BillingResponseCode.BILLING_UNAVAILABLE, "Billing is disabled")
}
extraParams?.size()
skuBundle?.size()
Log.d(TAG, "getSkuDetailsExtraParams(apiVersion=$apiVersion, packageName=$packageName, type=$type, skusBundle=$skuBundle, extraParams=$extraParams)")
val account = try {
getPreferredAccount(extraParams)
} catch (e: RuntimeException) {
return resultBundle(BillingResponseCode.BILLING_UNAVAILABLE, e.message)
}
val idList = skuBundle?.getStringArrayList("ITEM_ID_LIST")
val dynamicPriceTokensList = skuBundle?.getStringArrayList("DYNAMIC_PRICE_TOKENS_LIST")
if (idList.isNullOrEmpty()) {
Log.e(TAG, "Input Error: skusBundle must contain an array associated with key ITEM_ID_LIST.")
return resultBundle(BillingResponseCode.DEVELOPER_ERROR, "SKU bundle must contain sku list")
}
idList.sort()
if (dynamicPriceTokensList != null && dynamicPriceTokensList.isEmpty()) {
Log.e(TAG, "Input Error: skusBundle array associated with key ITEM_ID_LIST or key DYNAMIC_PRICE_TOKENS_LIST cannot be empty.")
return resultBundle(BillingResponseCode.DEVELOPER_ERROR, "SKU bundle must contain sku list")
}
if (apiVersion < 9 && extraParams?.isEmpty == false) {
return resultBundle(BillingResponseCode.DEVELOPER_ERROR, "Must specify an API version >= 9 to use this API.")
}
val params = GetSkuDetailsParams(
apiVersion = apiVersion,
skuType = type!!,
skuIdList = idList,
skuPkgName = extraParams?.getString("SKU_PACKAGE_NAME")?.also {
extraParams.remove("SKU_PACKAGE_NAME")
} ?: "",
sdkVersion = extraParams?.getString("playBillingLibraryVersion") ?: "",
multiOfferSkuDetail = extraParams?.let { bundleToMap(it) } ?: emptyMap()
)
val coreResult = try {
val deferred = CoroutineScope(Dispatchers.IO).async {
createIAPCore(context, account, packageName!!).getSkuDetails(params)
}
runBlocking { deferred.await() }
} catch (e: RuntimeException) {
Log.e(TAG, "getSkuDetailsExtraParams", e)
return resultBundle(BillingResponseCode.DEVELOPER_ERROR, e.message)
} catch (e: Exception) {
Log.e(TAG, "getSkuDetailsExtraParams", e)
return resultBundle(BillingResponseCode.DEVELOPER_ERROR, "Internal error.")
}
coreResult.let { detailsResult ->
val details = ArrayList(detailsResult.skuDetailsList.map { it.jsonDetails })
if (detailsResult.getCode() == BillingResponseCode.OK) {
return resultBundle(BillingResponseCode.OK, "", bundleOf("DETAILS_LIST" to details))
} else {
return resultBundle(detailsResult.getCode(), detailsResult.getMessage())
}
}
}
override fun acknowledgePurchase(
apiVersion: Int,
packageName: String?,
purchaseToken: String?,
extraParams: Bundle?
): Bundle {
if (!VendingPreferences.isBillingEnabled(context)) {
Log.w(TAG, "acknowledgePurchase: Billing is disabled")
return resultBundle(BillingResponseCode.BILLING_UNAVAILABLE, "Billing is disabled")
}
extraParams?.size()
Log.d(TAG, "acknowledgePurchase(apiVersion=$apiVersion, packageName=$packageName, purchaseToken=$purchaseToken, extraParams=$extraParams)")
val account = try {
getPreferredAccount(extraParams)
} catch (e: RuntimeException) {
return resultBundle(BillingResponseCode.BILLING_UNAVAILABLE, e.message)
}
val params = AcknowledgePurchaseParams(
apiVersion = apiVersion,
purchaseToken = purchaseToken!!,
extraParams = bundleToMap(extraParams)
)
val coreResult = try {
val deferred = CoroutineScope(Dispatchers.IO).async {
val coreResult = createIAPCore(context, account, packageName!!).acknowledgePurchase(params)
if (coreResult.getCode() == BillingResponseCode.OK && coreResult.purchaseItem != null) {
PurchaseManager.updatePurchase(coreResult.purchaseItem)
}
coreResult
}
runBlocking { deferred.await() }
} catch (e: RuntimeException) {
return resultBundle(BillingResponseCode.DEVELOPER_ERROR, e.message)
} catch (e: Exception) {
Log.e(TAG, "acknowledgePurchase", e)
return resultBundle(BillingResponseCode.DEVELOPER_ERROR, "Internal error.")
}
return coreResult.resultMap.toBundle()
}
override fun o(
apiVersion: Int,
packageName: String?,
arg3: String?,
extraParams: Bundle?
): Bundle {
extraParams?.size()
Log.d(TAG, "o(apiVersion=$apiVersion, packageName=$packageName, arg3=$arg3, extraParams=$extraParams)")
return resultBundle(BillingResponseCode.BILLING_UNAVAILABLE, "Not yet implemented")
}
override fun showInAppMessages(apiVersion: Int, packageName: String?, extraParams: Bundle?, callback: IInAppBillingServiceCallback?) {
Log.d(TAG, "showInAppMessages Not yet implemented")
}
override fun getBillingConfig(apiVersion: Int, packageName: String?, bundle: Bundle?, callback: IInAppBillingGetBillingConfigCallback) {
Log.d(TAG, "getBillingConfig apiVersion:$apiVersion packageName:$packageName bundle:$bundle")
val result = resultBundle(BillingResponseCode.OK, "", bundleOf(
"BILLING_CONFIG" to JSONObject().apply { put("countryCode", Locale.getDefault().country) }.toString()
))
callback.callback(result)
}
override fun isAlternativeBillingOnlyAvailable(
apiVersion: Int,
packageName: String?,
extraParams: Bundle?,
callback: IInAppBillingIsAlternativeBillingOnlyAvailableCallback?
) {
Log.d(TAG, "isAlternativeBillingOnlyAvailable Not yet implemented")
}
override fun createAlternativeBillingOnlyToken(
apiVersion: Int,
packageName: String?,
extraParams: Bundle?,
callback: IInAppBillingCreateAlternativeBillingOnlyTokenCallback?
) {
Log.d(TAG, "createAlternativeBillingOnlyToken Not yet implemented")
}
override fun getAlternativeBillingOnlyDialogIntent(
apiVersion: Int,
packageName: String?,
extraParams: Bundle?,
callback: IInAppBillingGetAlternativeBillingOnlyDialogIntentCallback?
) {
Log.d(TAG, "getAlternativeBillingOnlyDialogIntent Not yet implemented")
callback?.callback(Bundle())
}
override fun isExternalOfferAvailable(
apiVersion: Int,
packageName: String?,
extraParams: Bundle?,
callback: IInAppBillingIsExternalPaymentAvailableCallback?
) {
Log.d(TAG, "isExternalOfferAvailable Not yet implemented")
}
override fun createExternalOfferReportingDetails(
apiVersion: Int,
packageName: String?,
extraParams: Bundle?,
callback: IInAppBillingCreateExternalPaymentReportingDetailsCallback?
) {
Log.d(TAG, "createExternalOfferReportingDetails Not yet implemented")
}
override fun showExternalOfferInformationDialog(
apiVersion: Int,
packageName: String?,
extraParams: Bundle?,
callback: IInAppBillingGetExternalPaymentDialogIntentCallback?
) {
Log.d(TAG, "showExternalOfferInformationDialog Not yet implemented")
}
override fun delegateToBackend(bundle: Bundle?, callback: IInAppBillingDelegateToBackendCallback?) {
Log.d(TAG, "delegateToBackend Not yet implemented")
}
}

View file

@ -0,0 +1,62 @@
/*
* SPDX-FileCopyrightText: 2023 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.vending.billing;
import static org.microg.vending.billing.ui.PlayWebViewActivityKt.KEY_WEB_VIEW_ACCOUNT;
import static org.microg.vending.billing.ui.PlayWebViewActivityKt.KEY_WEB_VIEW_ACTION;
import static org.microg.vending.billing.ui.PlayWebViewActivityKt.KEY_WEB_VIEW_OPEN_URL;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.text.TextUtils;
import android.widget.Toast;
import androidx.annotation.Nullable;
import com.android.vending.R;
import org.microg.gms.auth.AuthConstants;
import org.microg.vending.billing.ui.PlayWebViewActivity;
import org.microg.vending.billing.ui.WebViewAction;
public class PurchaseActivity extends Activity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
String authAccount = getIntent().getStringExtra("authAccount");
AccountManager accountManager = AccountManager.get(this);
Account[] accounts = accountManager.getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE);
String referralUrl = getIntent().getStringExtra("referral_url");
if (!TextUtils.isEmpty(referralUrl) && accounts.length > 0) {
Account currAccount = null;
for (Account account : accounts) {
if (account.name.equals(authAccount)) {
currAccount = account;
break;
}
}
if (!referralUrl.startsWith("https")) {
referralUrl = referralUrl.replace("http", "https");
}
//Perform host judgment on URLs to prevent risky URLs from being exploited
if (referralUrl.startsWith("https://play.google.com/store")) {
Intent intent = new Intent(this, PlayWebViewActivity.class);
intent.putExtra(KEY_WEB_VIEW_ACTION, WebViewAction.OPEN_GP_PRODUCT_DETAIL.toString());
intent.putExtra(KEY_WEB_VIEW_OPEN_URL, referralUrl);
intent.putExtra(KEY_WEB_VIEW_ACCOUNT, currAccount);
startActivity(intent);
} else {
Toast.makeText(this, getString(R.string.pay_disabled), Toast.LENGTH_SHORT).show();
}
} else {
Toast.makeText(this, getString(R.string.pay_disabled), Toast.LENGTH_SHORT).show();
}
finish();
}
}

View file

@ -0,0 +1,129 @@
/*
* SPDX-FileCopyrightText: 2024 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.vending.billing
import android.accounts.Account
import android.content.ContentValues
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import org.microg.vending.billing.core.PurchaseItem
object PurchaseManager {
private val database by lazy { PurchaseDB(ContextProvider.context) }
fun queryPurchases(account: Account, pkgName: String, type: String): List<PurchaseItem> =
database.queryPurchases(account, pkgName, type)
fun updatePurchase(purchaseItem: PurchaseItem) = database.updatePurchase(purchaseItem)
fun removePurchase(purchaseToken: String) = database.removePurchase(purchaseToken)
fun addPurchase(account: Account, pkgName: String, purchaseItem: PurchaseItem) =
database.addPurchase(account, pkgName, purchaseItem)
private class PurchaseDB(mContext: Context?) : SQLiteOpenHelper(
mContext, DATABASE_NAME, null, DATABASE_VERSION
) {
@Synchronized
fun queryPurchases(account: Account, pkgName: String, type: String): List<PurchaseItem> {
val result = mutableListOf<PurchaseItem>()
val cursor = readableDatabase.query(
PURCHASE_TABLE,
null,
"account=? and package_name=? and type=?",
arrayOf(account.name, pkgName, type),
null,
null,
null
)
cursor?.use {
while (it.moveToNext()) {
val item = PurchaseItem(
it.getString(2),
it.getString(3),
it.getString(1),
it.getString(4),
it.getInt(5),
it.getString(6),
it.getString(7),
it.getLong(8),
it.getLong(9)
)
result.add(item)
}
}
return result
}
@Synchronized
fun updatePurchase(purchaseItem: PurchaseItem): Int {
val upItem = ContentValues()
upItem.put("purchase_state", purchaseItem.purchaseState)
upItem.put("json_data", purchaseItem.jsonData)
upItem.put("signature", purchaseItem.signature)
return writableDatabase.update(
PURCHASE_TABLE,
upItem,
"purchase_token=?",
arrayOf(purchaseItem.purchaseToken)
)
}
@Synchronized
fun removePurchase(purchaseToken: String) {
writableDatabase.delete(PURCHASE_TABLE, "purchase_token=?", arrayOf(purchaseToken))
}
@Synchronized
fun addPurchase(account: Account, pkgName: String, purchaseItem: PurchaseItem) {
val cv = ContentValues()
cv.put("account", account.name)
cv.put("package_name", pkgName)
cv.put("type", purchaseItem.type)
cv.put("sku", purchaseItem.sku)
cv.put("purchase_token", purchaseItem.purchaseToken)
cv.put("purchase_state", purchaseItem.purchaseState)
cv.put("json_data", purchaseItem.jsonData)
cv.put("signature", purchaseItem.signature)
cv.put("start_at", purchaseItem.startAt)
cv.put("expire_at", purchaseItem.expireAt)
writableDatabase.insertWithOnConflict(
PURCHASE_TABLE,
null,
cv,
SQLiteDatabase.CONFLICT_REPLACE
)
}
override fun onCreate(db: SQLiteDatabase) {
db.execSQL(CREATE_TABLE_PURCHASES)
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {}
companion object {
private const val TAG = "PurchaseDB"
private const val DATABASE_NAME = "purchase.db"
private const val PURCHASE_TABLE = "purchases"
private const val DATABASE_VERSION = 1
private const val CREATE_TABLE_PURCHASES =
"CREATE TABLE IF NOT EXISTS $PURCHASE_TABLE ( " +
"account TEXT, " +
"package_name TEXT, " +
"type TEXT, " +
"sku TEXT, " +
"purchase_token TEXT, " +
"purchase_state INTEGER, " +
"json_data TEXT, " +
"signature TEXT, " +
"start_at INTEGER, " +
"expire_at INTEGER, " +
"PRIMARY KEY (purchase_token));"
}
}
}

View file

@ -0,0 +1,31 @@
/*
* SPDX-FileCopyrightText: 2024 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.vending.billing
import android.content.Context
import android.content.SharedPreferences
import android.util.Log
import androidx.preference.PreferenceManager
// TODO: Better name?
private const val AUTH_STATUS_KEY = "key_auth_status"
class SettingsManager(private val context: Context) {
private val preferences: SharedPreferences by lazy {
PreferenceManager.getDefaultSharedPreferences(context.applicationContext)
}
fun setAuthStatus(needAuth: Boolean) {
if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "setAuthStatus: $needAuth")
val editor = preferences.edit()
editor.putBoolean(AUTH_STATUS_KEY, needAuth)
editor.apply()
}
fun getAuthStatus(): Boolean {
return preferences.getBoolean(AUTH_STATUS_KEY, true)
}
}

View file

@ -0,0 +1,341 @@
/*
* SPDX-FileCopyrightText: 2024 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.vending.billing
import android.accounts.Account
import android.accounts.AccountManager
import android.annotation.SuppressLint
import android.content.Context
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.icu.util.TimeZone
import android.net.ConnectivityManager
import android.net.Uri
import android.os.Build.VERSION.SDK_INT
import android.os.Bundle
import android.os.SystemClock
import android.provider.Settings
import android.util.Base64
import android.util.Log
import android.view.WindowManager
import androidx.core.app.ActivityCompat
import androidx.core.os.bundleOf
import com.android.billingclient.api.BillingClient.BillingResponseCode
import org.microg.gms.profile.Build
import org.microg.gms.profile.ProfileManager
import org.microg.gms.utils.digest
import org.microg.gms.utils.getExtendedPackageInfo
import org.microg.gms.utils.toBase64
import org.microg.vending.billing.core.*
import java.util.*
import kotlin.collections.Collection
import kotlin.collections.List
import kotlin.collections.Map
import kotlin.collections.Set
import kotlin.collections.any
import kotlin.collections.filter
import kotlin.collections.firstOrNull
import kotlin.collections.joinToString
import kotlin.collections.map
import kotlin.collections.mutableListOf
import kotlin.collections.mutableMapOf
import kotlin.collections.set
import kotlin.collections.toByteArray
import kotlin.collections.toList
import kotlin.collections.toSet
import kotlin.collections.toTypedArray
fun Map<String, Any?>.toBundle(): Bundle = bundleOf(*this.toList().toTypedArray())
/**
* Returns true if the receiving collection contains any of the specified elements.
*
* @param elements the elements to look for in the receiving collection.
* @return true if any element in [elements] is found in the receiving collection.
*/
fun <T> Collection<T>.containsAny(vararg elements: T): Boolean {
return containsAny(elements.toSet())
}
/**
* Returns true if the receiving collection contains any of the elements in the specified collection.
*
* @param elements the elements to look for in the receiving collection.
* @return true if any element in [elements] is found in the receiving collection.
*/
fun <T> Collection<T>.containsAny(elements: Collection<T>): Boolean {
val set = if (elements is Set) elements else elements.toSet()
return any(set::contains)
}
fun String.decodeHex(): ByteArray {
check(length % 2 == 0) { "Must have an even length" }
return chunked(2)
.map { it.toInt(16).toByte() }
.toByteArray()
}
fun resultBundle(@BillingResponseCode code: Int, msg: String?, data: Bundle = Bundle.EMPTY): Bundle {
val res = bundleOf(
"RESPONSE_CODE" to code,
"DEBUG_MESSAGE" to msg
)
res.putAll(data)
Log.d(TAG, "Result: $res")
return res
}
@SuppressLint("MissingPermission")
fun getDeviceIdentifier(context: Context): String {
// TODO: Improve dummy data
val deviceId = DeviceIdentifier.meid /*try {
(context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager?)?.let {
it.subscriberId ?: it.deviceId
}
} catch (e: Exception) {
null
}*/
if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "getDeviceIdentifier deviceId: $deviceId")
return deviceId.toByteArray(Charsets.UTF_8).digest("SHA-1").toBase64(Base64.URL_SAFE + Base64.NO_WRAP + Base64.NO_PADDING)
}
fun getGoogleAccount(context: Context, name: String? = null): Account? {
var accounts =
AccountManager.get(context).getAccountsByType(DEFAULT_ACCOUNT_TYPE).toList()
name?.let { accounts = accounts.filter { it.name == name } }
if (accounts.isEmpty())
return null
return accounts[0]
}
fun createClient(context: Context, pkgName: String): ClientInfo? {
return try {
val packageInfo = context.packageManager.getExtendedPackageInfo(pkgName)
ClientInfo(
pkgName,
packageInfo.certificates.firstOrNull()?.digest("MD5")?.toBase64(Base64.URL_SAFE + Base64.NO_WRAP + Base64.NO_PADDING) ?: "",
packageInfo.shortVersionCode
)
} catch (e: Exception) {
if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "createClient", e)
null
}
}
fun bundleToMap(bundle: Bundle?): Map<String, Any> {
val result = mutableMapOf<String, Any>()
if (bundle == null)
return result
for (key in bundle.keySet()) {
bundle.get(key)?.let {
result[key] = it
}
}
return result
}
fun getDisplayInfo(context: Context): DisplayMetrics? {
return try {
val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
if (windowManager != null) {
val displayMetrics = android.util.DisplayMetrics()
windowManager.defaultDisplay.getRealMetrics(displayMetrics)
return DisplayMetrics(
displayMetrics.widthPixels,
displayMetrics.heightPixels,
displayMetrics.xdpi,
displayMetrics.ydpi,
displayMetrics.densityDpi
)
}
return DisplayMetrics(
context.resources.displayMetrics.widthPixels,
context.resources.displayMetrics.heightPixels,
context.resources.displayMetrics.xdpi,
context.resources.displayMetrics.ydpi,
context.resources.displayMetrics.densityDpi
)
} catch (e: Exception) {
null
}
}
// TODO: Improve privacy
fun getBatteryLevel(context: Context): Int {
var batteryLevel = -1;
val intentFilter = IntentFilter("android.intent.action.BATTERY_CHANGED")
context.registerReceiver(null, intentFilter)?.let {
val level = it.getIntExtra("level", -1)
val scale = it.getIntExtra("scale", -1)
if (scale > 0) {
batteryLevel = level * 100 / scale
}
}
if (batteryLevel == -1 && SDK_INT >= 33) {
context.registerReceiver(null, intentFilter, Context.RECEIVER_EXPORTED)?.let {
val level = it.getIntExtra("level", -1)
val scale = it.getIntExtra("scale", -1)
if (scale > 0) {
batteryLevel = level * 100 / scale
}
}
}
return batteryLevel
}
fun getTelephonyData(context: Context): TelephonyData? {
// TODO: Dummy data
return null /*try {
context.getSystemService(Context.TELEPHONY_SERVICE)?.let {
val telephonyManager = it as TelephonyManager
return TelephonyData(
telephonyManager.simOperatorName!!,
DeviceIdentifier.meid,
telephonyManager.networkOperator!!,
telephonyManager.simOperator!!,
telephonyManager.phoneType
)
}
} catch (e: Exception) {
if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "getTelephonyData", e)
null
}*/
}
fun hasPermissions(context: Context, permissions: List<String>): Boolean {
for (permission in permissions) {
if (ActivityCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_GRANTED)
return false
}
return true
}
@SuppressLint("MissingPermission")
fun getLocationData(context: Context): LocationData? {
// TODO: Dummy data
return null /*try {
(context.getSystemService(Context.LOCATION_SERVICE) as LocationManager?)?.let { locationManager ->
if (hasPermissions(
context,
listOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
)
)
) {
locationManager.getLastKnownLocation("network")?.let { location ->
return LocationData(
location.altitude,
location.latitude,
location.longitude,
location.accuracy,
location.time.toDouble()
)
}
} else {
null
}
}
} catch (e: Exception) {
if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "getLocationData", e)
null
}*/
}
fun getNetworkData(context: Context): NetworkData {
val connectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager?
var linkDownstreamBandwidth: Long = 0
var linkUpstreamBandwidth: Long = 0
// TODO: Dummy data
/*
if (hasPermissions(context, listOf(Manifest.permission.ACCESS_NETWORK_STATE)) && SDK_INT >= 23) {
connectivityManager?.getNetworkCapabilities(connectivityManager.activeNetwork)?.let {
linkDownstreamBandwidth = (it.linkDownstreamBandwidthKbps * 1000 / 8).toLong()
linkUpstreamBandwidth = (it.linkUpstreamBandwidthKbps * 1000 / 8).toLong()
}
}
*/
val isActiveNetworkMetered = connectivityManager?.isActiveNetworkMetered ?: false
val netAddressList = mutableListOf<String>()
// TODO: Dummy data
/*try {
NetworkInterface.getNetworkInterfaces()?.let { enumeration ->
while (true) {
if (!enumeration.hasMoreElements()) {
break
}
val enumeration1 = enumeration.nextElement().inetAddresses
while (enumeration1.hasMoreElements()) {
val inetAddress = enumeration1.nextElement() as InetAddress
if (inetAddress.isLoopbackAddress) {
continue
}
netAddressList.add(inetAddress.hostAddress)
}
}
}
} catch (socketException: NullPointerException) {
if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "getNetworkData:${socketException.message}")
}*/
return NetworkData(
linkDownstreamBandwidth,
linkUpstreamBandwidth,
isActiveNetworkMetered,
netAddressList
)
}
@SuppressLint("HardwareIds")
fun getAndroidId(context: Context): String {
return Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID) ?: ""
}
fun getUserAgent(): String {
return "Android-Finsky/${Uri.encode(VENDING_VERSION_NAME)} (api=3,versionCode=$VENDING_VERSION_CODE,sdk=${Build.VERSION.SDK_INT},device=${Build.DEVICE},hardware=${Build.HARDWARE},product=${Build.PRODUCT},platformVersionRelease=${Build.VERSION.RELEASE},model=${Uri.encode(Build.MODEL)},buildId=${Build.ID},isWideScreen=0,supportedAbis=${Build.SUPPORTED_ABIS.joinToString(";")})"
}
fun createDeviceEnvInfo(context: Context): DeviceEnvInfo? {
try {
val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)
return DeviceEnvInfo(
gpVersionCode = VENDING_VERSION_CODE,
gpVersionName = VENDING_VERSION_NAME,
gpPkgName = VENDING_PACKAGE_NAME,
androidId = getAndroidId(context),
biometricSupport = true,
biometricSupportCDD = true,
deviceId = getDeviceIdentifier(context),
serialNo = Build.SERIAL ?: "",
locale = Locale.getDefault(),
userAgent = getUserAgent(),
gpLastUpdateTime = packageInfo.lastUpdateTime,
gpFirstInstallTime = packageInfo.firstInstallTime,
gpSourceDir = packageInfo.applicationInfo!!.sourceDir!!,
device = Build.DEVICE ?: "",
displayMetrics = getDisplayInfo(context),
telephonyData = getTelephonyData(context),
product = Build.PRODUCT ?: "",
model = Build.MODEL ?: "",
manufacturer = Build.MANUFACTURER ?: "",
fingerprint = Build.FINGERPRINT ?: "",
release = Build.VERSION.RELEASE ?: "",
brand = Build.BRAND ?: "",
batteryLevel = getBatteryLevel(context),
timeZoneOffset = if (SDK_INT >= 24) TimeZone.getDefault().rawOffset.toLong() else 0,
locationData = getLocationData(context),
isAdbEnabled = false, //Settings.Global.getInt(context.contentResolver, "adb_enabled", 0) == 1,
installNonMarketApps = true, //Settings.Secure.getInt(context.contentResolver, "install_non_market_apps", 0) == 1,
networkData = getNetworkData(context),
uptimeMillis = SystemClock.uptimeMillis(),
timeZoneDisplayName = if (SDK_INT >= 24) TimeZone.getDefault().displayName!! else "",
googleAccounts = AccountManager.get(context).getAccountsByType(DEFAULT_ACCOUNT_TYPE).map { it.name }
)
} catch (e: Exception) {
if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "createDeviceInfo", e)
return null
}
}

View file

@ -0,0 +1,7 @@
package org.microg.vending.billing.core
data class AcknowledgePurchaseParams(
val apiVersion: Int,
val purchaseToken: String,
val extraParams: Map<String, Any> = emptyMap()
)

View file

@ -0,0 +1,36 @@
package org.microg.vending.billing.core
import org.microg.vending.billing.proto.AcknowledgePurchaseResponse
class AcknowledgePurchaseResult(
val purchaseItem: PurchaseItem? = null,
resultMap: Map<String, Any> = mapOf(
"RESPONSE_CODE" to 0,
"DEBUG_MESSAGE" to ""
)
) : IAPResult(resultMap) {
companion object {
fun parseFrom(
response: AcknowledgePurchaseResponse?
): AcknowledgePurchaseResult {
if (response == null) {
throw NullPointerException("response is null")
}
if (response.failedResponse != null) {
return AcknowledgePurchaseResult(
null,
mapOf(
"RESPONSE_CODE" to response.failedResponse.statusCode,
"DEBUG_MESSAGE" to response.failedResponse.msg
)
)
}
if (response.purchaseItem == null) {
throw NullPointerException("AcknowledgePurchaseResponse PurchaseItem is null")
}
if (response.purchaseItem.purchaseItemData.size != 1)
throw IllegalStateException("AcknowledgePurchaseResult purchase item count != 1")
return AcknowledgePurchaseResult(parsePurchaseItem(response.purchaseItem).getOrNull(0))
}
}
}

View file

@ -0,0 +1,9 @@
package org.microg.vending.billing.core
data class AcquireParams(
val buyFlowParams: BuyFlowParams,
val actionContext: List<ByteArray> = emptyList(),
val droidGuardResult: String? = null,
val authToken: String? = null,
var lastAcquireResult: AcquireResult? = null
)

View file

@ -0,0 +1,29 @@
package org.microg.vending.billing.core
import org.microg.vending.billing.core.ui.AcquireParsedResult
import org.microg.vending.billing.core.ui.parseAcquireResponse
import org.microg.vending.billing.proto.AcquireRequest
import org.microg.vending.billing.proto.AcquireResponse
data class AcquireResult(
val acquireParsedResult: AcquireParsedResult,
val acquireRequest: AcquireRequest,
val acquireResponse: AcquireResponse,
) {
companion object {
fun parseFrom(
acquireParams: AcquireParams,
acquireRequest: AcquireRequest,
acquireResponse: AcquireResponse?
): AcquireResult {
if (acquireResponse == null) {
throw NullPointerException("AcquireResponse is null")
}
return AcquireResult(
parseAcquireResponse(acquireParams, acquireResponse),
acquireRequest,
acquireResponse
)
}
}
}

View file

@ -0,0 +1,11 @@
package org.microg.vending.billing.core
data class AuthData(
val email: String,
val authToken: String,
val gsfId: String = "",
val deviceCheckInConsistencyToken: String = "",
val deviceConfigToken: String = "",
val experimentsConfigToken: String = "",
val dfeCookie: String = ""
)

View file

@ -0,0 +1,15 @@
package org.microg.vending.billing.core
data class BuyFlowParams(
val apiVersion: Int,
val sku: String,
val skuType: String,
val developerPayload: String = "",
val sdkVersion: String = "",
val needAuth: Boolean = false,
val skuParams: Map<String, Any> = emptyMap(),
val skuSerializedDockIdList: List<String>? = null,
val skuOfferIdTokenList: List<String>? = null,
val oldSkuPurchaseToken: String? = null,
val oldSkuPurchaseId: String? = null
)

View file

@ -0,0 +1,7 @@
package org.microg.vending.billing.core
data class ClientInfo(
val pkgName: String,
val signatureMD5: String,
val versionCode: Int
)

View file

@ -0,0 +1,7 @@
package org.microg.vending.billing.core
data class ConsumePurchaseParams(
val apiVersion: Int,
val purchaseToken: String,
val extraParams: Map<String, Any> = emptyMap()
)

View file

@ -0,0 +1,30 @@
package org.microg.vending.billing.core
import org.microg.vending.billing.proto.ConsumePurchaseResponse
class ConsumePurchaseResult(
resultMap: Map<String, Any> = mapOf(
"RESPONSE_CODE" to 0,
"DEBUG_MESSAGE" to ""
)
) : IAPResult(resultMap) {
companion object {
fun parseFrom(
consumePurchaseResponse: ConsumePurchaseResponse?
): ConsumePurchaseResult {
if (consumePurchaseResponse == null) {
throw NullPointerException("consumePurchaseResponse is null")
}
if (consumePurchaseResponse.failedResponse != null) {
return ConsumePurchaseResult(
mapOf(
"RESPONSE_CODE" to consumePurchaseResponse.failedResponse.statusCode,
"DEBUG_MESSAGE" to consumePurchaseResponse.failedResponse.msg
)
)
}
return ConsumePurchaseResult()
}
}
}

View file

@ -0,0 +1,68 @@
package org.microg.vending.billing.core
import java.util.Locale
data class DeviceEnvInfo(
val gpVersionCode: Long,
val gpVersionName: String,
val gpPkgName: String,
val gpLastUpdateTime: Long,
val gpFirstInstallTime: Long,
val gpSourceDir: String,
val androidId: String,
val biometricSupport: Boolean,
val biometricSupportCDD: Boolean,
val deviceId: String,
val serialNo: String,
val locale: Locale,
val userAgent: String,
val device: String,
val displayMetrics: DisplayMetrics?,
val telephonyData: TelephonyData?,
val locationData: LocationData?,
val networkData: NetworkData?,
val product: String,
val model: String,
val manufacturer: String,
val fingerprint: String,
val release: String,
val brand: String,
val batteryLevel: Int,
val timeZoneOffset: Long,
val isAdbEnabled: Boolean,
val installNonMarketApps: Boolean,
val uptimeMillis: Long,
val timeZoneDisplayName: String,
val googleAccounts: List<String>
)
data class DisplayMetrics(
val widthPixels: Int,
val heightPixels: Int,
val xdpi: Float,
val ydpi: Float,
val densityDpi: Int
)
data class TelephonyData(
val simOperatorName: String,
val phoneDeviceId: String,
val networkOperator: String,
val simOperator: String,
val phoneType: Int = -1
)
data class LocationData(
val altitude: Double,
val latitude: Double,
val longitude: Double,
val accuracy: Float,
val time: Double
)
data class NetworkData(
val linkDownstreamBandwidth: Long,
val linkUpstreamBandwidth: Long,
val isActiveNetworkMetered: Boolean,
val netAddressList: List<String>,
)

View file

@ -0,0 +1,8 @@
package org.microg.vending.billing.core
data class GetPurchaseHistoryParams(
val apiVersion: Int,
val type: String,
val continuationToken: String? = null,
val extraParams: Map<String, Any> = emptyMap()
)

View file

@ -0,0 +1,53 @@
package org.microg.vending.billing.core
import org.microg.vending.billing.proto.PurchaseHistoryResponse
class GetPurchaseHistoryResult(
val purchaseHistoryList: List<PurchaseHistoryItem>?,
val continuationToken: String?,
resultMap: Map<String, Any> = mapOf(
"RESPONSE_CODE" to 0,
"DEBUG_MESSAGE" to ""
)
) : IAPResult(resultMap) {
companion object {
fun parseFrom(
response: PurchaseHistoryResponse?
): GetPurchaseHistoryResult {
if (response == null) {
throw NullPointerException("PurchaseHistoryResponse is null")
}
if (response.failedResponse != null) {
return GetPurchaseHistoryResult(
null,
null,
mapOf(
"RESPONSE_CODE" to response.failedResponse.statusCode,
"DEBUG_MESSAGE" to response.failedResponse.msg
)
)
}
if (response.productId.size != response.purchaseJson.size || response.purchaseJson.size != response.signature.size) {
throw IllegalStateException("GetPurchaseHistoryResult item count error")
}
val purchaseHistoryList = mutableListOf<PurchaseHistoryItem>()
var continuationToken: String? = null
for (cnt in 0 until response.productId.size) {
purchaseHistoryList.add(
PurchaseHistoryItem(
response.productId[cnt],
response.purchaseJson[cnt],
response.signature[cnt]
)
)
}
if (!response.continuationToken.isNullOrEmpty()) {
continuationToken = response.continuationToken
}
return GetPurchaseHistoryResult(purchaseHistoryList, continuationToken)
}
}
class PurchaseHistoryItem(val sku: String, val jsonData: String, val signature: String)
}

View file

@ -0,0 +1,10 @@
package org.microg.vending.billing.core
data class GetSkuDetailsParams(
val apiVersion: Int,
val skuType: String,
val skuIdList: List<String>,
val skuPkgName: String = "",
val sdkVersion: String = "",
val multiOfferSkuDetail: Map<String, Any> = emptyMap()
)

View file

@ -0,0 +1,45 @@
package org.microg.vending.billing.core
import android.util.Log
import org.microg.vending.billing.proto.DocId
import org.microg.vending.billing.proto.SkuDetailsResponse
import org.microg.vending.billing.proto.SkuInfo
class GetSkuDetailsResult private constructor(
val skuDetailsList: List<SkuDetailsItem>,
resultMap: Map<String, Any> = mapOf("RESPONSE_CODE" to 0, "DEBUG_MESSAGE" to "")
) : IAPResult(resultMap) {
companion object {
fun parseFrom(skuDetailsResponse: SkuDetailsResponse?): GetSkuDetailsResult {
if (skuDetailsResponse == null) {
throw NullPointerException("SkuDetailsResponse is null")
}
if (skuDetailsResponse.failedResponse != null) {
return GetSkuDetailsResult(
emptyList(),
mapOf(
"RESPONSE_CODE" to skuDetailsResponse.failedResponse.statusCode,
"DEBUG_MESSAGE" to skuDetailsResponse.failedResponse.msg
)
)
}
val skuDetailsList =
skuDetailsResponse.details.filter { it.skuDetails.isNotBlank() }
.map { skuDetails ->
val skuInfo = skuDetails.skuInfo ?: SkuInfo()
SkuDetailsItem(
skuDetails.skuDetails,
skuInfo.skuItem.associate { it.token to it.docId }
)
}
return GetSkuDetailsResult(skuDetailsList)
}
}
data class SkuDetailsItem(
val jsonDetails: String,
val docIdMap: Map<String, DocId?>
)
}

View file

@ -0,0 +1,21 @@
package org.microg.vending.billing.core
class GooglePlayApi {
companion object {
const val URL_BASE = "https://play-fe.googleapis.com"
const val URL_FDFE = "$URL_BASE/fdfe"
const val URL_SKU_DETAILS = "$URL_FDFE/skuDetails"
const val URL_EES_ACQUIRE = "$URL_FDFE/ees/acquire"
const val URL_ACKNOWLEDGE_PURCHASE = "$URL_FDFE/acknowledgePurchase"
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_ITEM_DETAILS = "$URL_FDFE/getItems"
const val URL_PURCHASE = "$URL_FDFE/purchase"
const val URL_DELIVERY = "$URL_FDFE/delivery"
const val URL_ENTERPRISE_CLIENT_POLICY = "$URL_FDFE/getEnterpriseClientPolicy"
const val URL_SYNC = "$URL_FDFE/sync"
const val URL_BULK = "$URL_FDFE/bulkGrantEntitlement"
}
}

View file

@ -0,0 +1,43 @@
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<String, String> {
val headers: MutableMap<String, String> = HashMap()
headers["Authorization"] = "Bearer " + authData.authToken
headers["User-Agent"] = deviceInfo.userAgent
return headers
}
fun getDefaultHeaders(authData: AuthData, deviceInfo: DeviceEnvInfo): MutableMap<String, String> {
val headers: MutableMap<String, String> = HashMap()
headers["Authorization"] = "Bearer " + authData.authToken
headers["User-Agent"] = deviceInfo.userAgent
headers["X-DFE-Device-Id"] = authData.gsfId
headers["Accept-Language"] = "${deviceInfo.locale.language}-${deviceInfo.locale.country}"
headers["X-Limit-Ad-Tracking-Enabled"] = "true"
headers["X-DFE-Network-Type"] = "4"
headers["X-DFE-Client-Id"] = "am-google"
// TODO: Magic constants?
headers["X-DFE-Phenotype"] =
"H4sIAAAAAAAAAOOKcXb0DQ4oNzCoKNV1c0zMsywL9PVwqvBPcsr2TykJ8HUv9gx1La6I9Dcw9k7xTYtIMnasSopIq0g0SI8IdwxwDbfIygxw8U-PdPR1THML1DXNS_L0yffOinRxtLWVYgAAjtXkomAAAAA"
headers["X-DFE-Encoded-Targets"] =
"CAEaSuMFBdCPgQYJxAIED+cBfS+6AVYBIQojDSI3hAEODGxYvQGMAhRMWQEVWxniBQSSAjycAuESkgrgBeAfgCv4KI8VgxHqGNxrRbkI"
headers["X-DFE-Request-Params"] = "timeoutMs=4000"
headers["X-Ad-Id"] = "00000000-0000-0000-0000-000000000000"
headers["Connection"] = "Keep-Alive"
if (deviceInfo.androidId.isNotBlank())
headers["x-public-android-id"] = deviceInfo.androidId
if (authData.dfeCookie.isNotBlank())
headers["x-dfe-cookie"] = authData.dfeCookie
if (authData.deviceCheckInConsistencyToken.isNotBlank()) {
headers["X-DFE-Device-Checkin-Consistency-Token"] =
authData.deviceCheckInConsistencyToken
}
return headers
}
}

View file

@ -0,0 +1,237 @@
package org.microg.vending.billing.core
import android.content.Context
import android.net.Uri
import android.util.Log
import com.squareup.wire.Message
import com.squareup.wire.ProtoAdapter
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.engine.okhttp.OkHttp
import io.ktor.client.plugins.HttpTimeout
import io.ktor.client.plugins.cache.HttpCache
import io.ktor.client.plugins.timeout
import io.ktor.client.request.forms.submitForm
import io.ktor.client.request.get
import io.ktor.client.request.headers
import io.ktor.client.request.post
import io.ktor.client.request.prepareGet
import io.ktor.client.request.setBody
import io.ktor.client.request.url
import io.ktor.http.HttpHeaders
import io.ktor.http.HttpStatusCode
import io.ktor.http.ParametersImpl
import io.ktor.http.URLBuilder
import io.ktor.http.Url
import io.ktor.utils.io.ByteReadChannel
import io.ktor.utils.io.pool.ByteArrayPool
import org.json.JSONObject
import org.microg.gms.utils.singleInstanceOf
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.OutputStream
private const val POST_TIMEOUT = 8000L
private const val TAG = "HttpClient"
class HttpClient {
private val client = singleInstanceOf { HttpClient(OkHttp) {
expectSuccess = true
install(HttpTimeout)
} }
private val clientWithCache = singleInstanceOf { HttpClient(OkHttp) {
expectSuccess = true
install(HttpCache)
install(HttpTimeout)
} }
suspend fun download(
url: String,
downloadFile: File,
params: Map<String, String> = emptyMap()
): File = downloadFile.also { toFile ->
val parentDir = downloadFile.getParentFile()
if (parentDir != null && !parentDir.exists() && !parentDir.mkdirs()) {
throw IOException("Failed to create directories: ${parentDir.absolutePath}")
}
FileOutputStream(toFile).use { download(url, it, params) }
}
suspend fun download(
url: String,
downloadTo: OutputStream,
params: Map<String, String> = emptyMap(),
downloadedBytes: Long = 0,
emitProgress: (bytesDownloaded: Long) -> Unit = {}
) {
try {
Log.d(TAG, "download downloadedBytes:$downloadedBytes")
client.prepareGet(url.asUrl(params)){
if (downloadedBytes > 0) {
headers {
append(HttpHeaders.Range, "bytes=$downloadedBytes-")
}
}
}.execute { response ->
val body: ByteReadChannel = response.body()
// Modified version of `ByteReadChannel.copyTo(OutputStream, Long)` to indicate progress
val buffer = ByteArrayPool.borrow()
try {
var copied = downloadedBytes
val bufferSize = buffer.size
do {
val rc = body.readAvailable(buffer, 0, bufferSize)
copied += rc
if (rc > 0) {
downloadTo.write(buffer, 0, rc)
emitProgress(copied)
}
} while (rc > 0)
} finally {
ByteArrayPool.recycle(buffer)
}
// don't close `downloadTo` yet
}
} catch (e: Exception) {
Log.w(TAG, "download error : $e")
throw e
}
}
suspend fun <O> get(
url: String,
headers: Map<String, String> = emptyMap(),
params: Map<String, String> = emptyMap(),
adapter: ProtoAdapter<O>,
cache: Boolean = true
): O {
val response = (if (cache) clientWithCache else client).get(url.asUrl(params)) {
headers {
headers.forEach {
append(it.key, it.value)
}
}
}
if (response.status != HttpStatusCode.OK) throw IOException("Server responded with status ${response.status}")
else return adapter.decode(response.body<ByteArray>())
}
/**
* Post empty body.
*/
suspend fun <I : Message<I, *>, O> post(
url: String,
headers: Map<String, String> = emptyMap(),
params: Map<String, String> = emptyMap(),
adapter: ProtoAdapter<O>,
cache: Boolean = false
): O {
val response = (if (cache) clientWithCache else client).post(url.asUrl(params)) {
setBody(ByteArray(0))
headers {
headers.forEach {
append(it.key, it.value)
}
append(HttpHeaders.ContentType, "application/x-protobuf")
}
timeout {
requestTimeoutMillis = POST_TIMEOUT
}
}
return adapter.decode(response.body<ByteArray>())
}
/**
* Post protobuf-encoded body.
*/
suspend fun <I : Message<I, *>, O> post(
url: String,
headers: Map<String, String> = emptyMap(),
params: Map<String, String> = emptyMap(),
payload: I,
adapter: ProtoAdapter<O>,
cache: Boolean = false
): O {
val response = (if (cache) clientWithCache else client).post(url.asUrl(params)) {
setBody(ByteReadChannel(payload.encode()))
headers {
headers.forEach {
append(it.key, it.value)
}
append(HttpHeaders.ContentType, "application/x-protobuf")
}
timeout {
requestTimeoutMillis = POST_TIMEOUT
}
}
return adapter.decode(response.body<ByteArray>())
}
/**
* Post JSON body.
*/
suspend fun post(
url: String,
headers: Map<String, String> = emptyMap(),
params: Map<String, String> = emptyMap(),
payload: JSONObject,
cache: Boolean = false
): JSONObject {
val response = (if (cache) clientWithCache else client).post(url.asUrl(params)) {
setBody(payload.toString())
headers {
headers.forEach {
append(it.key, it.value)
}
append(HttpHeaders.ContentType, "application/json")
}
timeout {
requestTimeoutMillis = POST_TIMEOUT
}
}
return JSONObject(response.body<String>())
}
/**
* Post form body.
*/
suspend fun <O> post(
url: String,
headers: Map<String, String> = emptyMap(),
params: Map<String, String> = emptyMap(),
form: Map<String, String> = emptyMap(),
adapter: ProtoAdapter<O>,
cache: Boolean = false
): O {
val response = (if (cache) clientWithCache else client).submitForm(
formParameters = ParametersImpl(form.mapValues { listOf(it.key) }),
encodeInQuery = false
) {
url(url.asUrl(params))
headers { // Content-Type is set to `x-www-form-urlencode` automatically
headers.forEach {
append(it.key, it.value)
}
}
timeout {
requestTimeoutMillis = POST_TIMEOUT
}
}
return adapter.decode(response.body<ByteArray>())
}
private fun String.asUrl(params: Map<String, String>): Url = URLBuilder(this).apply {
params.forEach {
parameters.append(it.key, it.value)
}
}.build()
}

Some files were not shown because too many files have changed in this diff Show more