Repo Created
This commit is contained in:
parent
eb305e2886
commit
a8c22c65db
4784 changed files with 329907 additions and 2 deletions
162
vending-app/build.gradle
Normal file
162
vending-app/build.gradle
Normal 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
9
vending-app/proguard-rules.pro
vendored
Normal 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
|
||||
23
vending-app/src/huawei/AndroidManifest.xml
Normal file
23
vending-app/src/huawei/AndroidManifest.xml
Normal 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>
|
||||
21
vending-app/src/huaweilh/AndroidManifest.xml
Normal file
21
vending-app/src/huaweilh/AndroidManifest.xml
Normal 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>
|
||||
323
vending-app/src/main/AndroidManifest.xml
Normal file
323
vending-app/src/main/AndroidManifest.xml
Normal 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>
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package com.google.android.gms.checkin.internal;
|
||||
|
||||
interface ICheckinService {
|
||||
String getDeviceDataVersionInfo();
|
||||
long getLastCheckinSuccessTime();
|
||||
String getLastSimOperator();
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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> {
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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";
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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));"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
341
vending-app/src/main/java/org/microg/vending/billing/Utils.kt
Normal file
341
vending-app/src/main/java/org/microg/vending/billing/Utils.kt
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
)
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = ""
|
||||
)
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package org.microg.vending.billing.core
|
||||
|
||||
data class ClientInfo(
|
||||
val pkgName: String,
|
||||
val signatureMD5: String,
|
||||
val versionCode: Int
|
||||
)
|
||||
|
|
@ -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()
|
||||
)
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>,
|
||||
)
|
||||
|
|
@ -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()
|
||||
)
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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()
|
||||
)
|
||||
|
|
@ -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?>
|
||||
)
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue