diff --git a/vending-app/build.gradle b/vending-app/build.gradle
new file mode 100644
index 00000000..8031b91d
--- /dev/null
+++ b/vending-app/build.gradle
@@ -0,0 +1,84 @@
+/*
+ * 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
+ }
+
+ 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
+ }
+
+ lintOptions {
+ disable 'MissingTranslation'
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_17
+ targetCompatibility JavaVersion.VERSION_17
+ }
+}
+
+dependencies {
+ implementation project(':fake-signature')
+ implementation project(':play-services-auth')
+
+ implementation "com.squareup.wire:wire-runtime:$wireVersion"
+ implementation "com.android.volley:volley:$volleyVersion"
+}
+
+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"
+ }
+}
\ No newline at end of file
diff --git a/vending-app/src/main/AndroidManifest.xml b/vending-app/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..7156d709
--- /dev/null
+++ b/vending-app/src/main/AndroidManifest.xml
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vending-app/src/main/java/com/android/vending/licensing/LicenseRequest.java b/vending-app/src/main/java/com/android/vending/licensing/LicenseRequest.java
new file mode 100644
index 00000000..08d6e28a
--- /dev/null
+++ b/vending-app/src/main/java/com/android/vending/licensing/LicenseRequest.java
@@ -0,0 +1,217 @@
+package com.android.vending.licensing;
+
+import static com.android.volley.Request.Method.GET;
+
+import android.util.Base64;
+import android.util.Log;
+
+import com.android.vending.AndroidVersionMeta;
+import com.android.vending.DeviceMeta;
+import com.android.vending.EncodedTriple;
+import com.android.vending.EncodedTripleWrapper;
+import com.android.vending.IntWrapper;
+import com.android.vending.LicenseRequestHeader;
+import com.android.vending.LicenseResult;
+import com.android.vending.Locality;
+import com.android.vending.LocalityWrapper;
+import com.android.vending.StringWrapper;
+import com.android.vending.Timestamp;
+import com.android.vending.TimestampContainer;
+import com.android.vending.TimestampContainer1;
+import com.android.vending.TimestampContainer1Wrapper;
+import com.android.vending.TimestampContainer2;
+import com.android.vending.TimestampStringWrapper;
+import com.android.vending.TimestampWrapper;
+import com.android.vending.UnknownByte12;
+import com.android.vending.UserAgent;
+import com.android.vending.Util;
+import com.android.vending.Uuid;
+import com.android.vending.V1Container;
+import com.android.volley.NetworkResponse;
+import com.android.volley.Request;
+import com.android.volley.Response;
+import com.android.volley.VolleyError;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.UUID;
+
+import okio.ByteString;
+
+public abstract class LicenseRequest extends Request {
+
+ private final String xPsRh;
+ private final String auth;
+ private static final String TAG = "FakeLicenseRequest";
+
+ private static final int BASE64_FLAGS = Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING;
+ private static final long ANDROID_ID = 1;
+
+ private final Response.Listener successListener;
+
+
+ protected LicenseRequest(String url, String auth, Response.Listener successListener, Response.ErrorListener errorListener) {
+ super(GET, url, errorListener);
+ this.auth = auth;
+
+ this.successListener = successListener;
+
+ long millis = System.currentTimeMillis();
+ TimestampContainer.Builder timestamp = new TimestampContainer.Builder()
+ .container2(new TimestampContainer2.Builder()
+ .wrapper(new TimestampWrapper.Builder().timestamp(makeTimestamp(millis)).build())
+ .timestamp(makeTimestamp(millis))
+ .build());
+ millis = System.currentTimeMillis();
+ timestamp
+ .container1Wrapper(new TimestampContainer1Wrapper.Builder()
+ .androidId(String.valueOf(ANDROID_ID))
+ .container(new TimestampContainer1.Builder()
+ .timestamp(millis + "000")
+ .wrapper(makeTimestamp(millis))
+ .build())
+ .build()
+ );
+ String encodedTimestamps = new String(
+ Base64.encode(Util.encodeGzip(timestamp.build().encode()), BASE64_FLAGS)
+ );
+
+ Locality locality = new Locality.Builder()
+ .unknown1(1)
+ .unknown2(2)
+ .countryCode("")
+ .region(new TimestampStringWrapper.Builder()
+ .string("").timestamp(makeTimestamp(System.currentTimeMillis())).build())
+ .country(new TimestampStringWrapper.Builder()
+ .string("").timestamp(makeTimestamp(System.currentTimeMillis())).build())
+ .unknown3(0)
+ .build();
+ String encodedLocality = new String(
+ Base64.encode(locality.encode(), BASE64_FLAGS)
+ );
+
+ byte[] header = new LicenseRequestHeader.Builder()
+ .encodedTimestamps(new StringWrapper.Builder().string(encodedTimestamps).build())
+ .triple(
+ new EncodedTripleWrapper.Builder().triple(
+ new EncodedTriple.Builder()
+ .encoded1("")
+ .encoded2("")
+ .empty("")
+ .build()
+ ).build()
+ )
+ .locality(new LocalityWrapper.Builder().encodedLocalityProto(encodedLocality).build())
+ .unknown(new IntWrapper.Builder().integer(5).build())
+ .empty("")
+ .deviceMeta(new DeviceMeta.Builder()
+ .android(
+ new AndroidVersionMeta.Builder()
+ .androidSdk(0)
+ .buildNumber("")
+ .androidVersion("")
+ .unknown(0)
+ .build()
+ )
+ .unknown1(new UnknownByte12.Builder().bytes(new ByteString(new byte[]{}
+ )).build())
+ .unknown2(1)
+ .build()
+ )
+ .userAgent(new UserAgent.Builder()
+ .deviceProductName("")
+ .deviceSoc("")
+ .deviceModelName("")
+ .finskyVersion("")
+ .deviceName("")
+ .androidId(ANDROID_ID) // must not be 0
+ .deviceSignature("")
+ .build()
+ )
+ .uuid(new Uuid.Builder()
+ .uuid(UUID.randomUUID().toString())
+ .unknown(2)
+ .build()
+ )
+ .build().encode();
+ this.xPsRh = new String(Base64.encode(Util.encodeGzip(header), BASE64_FLAGS));
+
+ //Log.d(TAG, "Product " + Build.PRODUCT + ", Board " + Build.BOARD + " Model " + Build.MODEL + " Device " + Build.DEVICE);
+
+ Log.v(TAG, "X-PS-RH: " + xPsRh);
+ }
+
+ @Override
+ public Map getHeaders() {
+ return Map.of(
+ "X-PS-RH", xPsRh,
+ "Authorization", "Bearer " + auth,
+ "Connection", "Keep-Alive"
+ );
+ }
+
+ @Override
+ protected void deliverResponse(T response) {
+ successListener.onResponse(response);
+ }
+
+ private static Timestamp makeTimestamp(long millis) {
+ return new Timestamp.Builder()
+ .seconds((int) (millis / 1000))
+ .nanos(Math.floorMod(millis, 1000) * 1000000)
+ .build();
+ }
+
+ public static class V1 extends LicenseRequest {
+
+ public V1(String packageName, String auth, int versionCode, long nonce, Response.Listener successListener, Response.ErrorListener errorListener) {
+ super("https://play-fe.googleapis.com/fdfe/apps/checkLicense?pkgn=" + packageName + "&vc=" + versionCode + "&nnc=" + nonce,
+ auth, successListener, errorListener
+ );
+ }
+
+ @Override
+ protected Response parseNetworkResponse(NetworkResponse response) {
+ if (response != null && response.data != null) {
+ try {
+ LicenseResult result = LicenseResult.ADAPTER.decode(response.data);
+ return Response.success(result.information.v1, null);
+ } catch (IOException e) {
+ return Response.error(new VolleyError(e));
+ } catch (NullPointerException e) {
+ // A field does not exist → user has no license
+ return Response.success(null, null);
+ }
+ } else {
+ return Response.error(new VolleyError("No response was returned"));
+ }
+ }
+ }
+
+ public static class V2 extends LicenseRequest {
+ public V2(String packageName, String auth, int versionCode, Response.Listener successListener,
+ Response.ErrorListener errorListener) {
+ super(
+ "https://play-fe.googleapis.com/fdfe/apps/checkLicenseServerFallback?pkgn=" + packageName + "&vc=" + versionCode,
+ auth, successListener, errorListener
+ );
+ }
+
+ @Override
+ protected Response parseNetworkResponse(NetworkResponse response) {
+ if (response != null && response.data != null) {
+ try {
+ LicenseResult result = LicenseResult.ADAPTER.decode(response.data);
+ return Response.success(result.information.v2.license.jwt, null);
+ } catch (IOException e) {
+ return Response.error(new VolleyError(e));
+ } catch (NullPointerException e) {
+ // A field does not exist → user has no license
+ return Response.success(null, null);
+ }
+ } else {
+ return Response.error(new VolleyError("No response was returned"));
+ }
+ }
+ }
+}
diff --git a/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java b/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java
new file mode 100644
index 00000000..267ddc84
--- /dev/null
+++ b/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java
@@ -0,0 +1,232 @@
+/*
+ * SPDX-FileCopyrightText: 2023 microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.android.vending.licensing;
+
+import static android.accounts.AccountManager.KEY_AUTHTOKEN;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.accounts.AuthenticatorException;
+import android.accounts.OperationCanceledException;
+import android.app.Service;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.volley.Request;
+import com.android.volley.RequestQueue;
+import com.android.volley.toolbox.Volley;
+
+import org.microg.gms.auth.AuthConstants;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+
+public class LicensingService extends Service {
+ private static final String TAG = "FakeLicenseService";
+ private RequestQueue queue;
+ private AccountManager accountManager;
+
+ private static final String KEY_V2_RESULT_JWT = "LICENSE_DATA";
+
+ private static final String AUTH_TOKEN_SCOPE = "oauth2:https://www.googleapis.com/auth/googleplay";
+
+ /* 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.
+ */
+ private static final int LICENSED = 0x0;
+ /**
+ * The application is licensed to the user, but there is an updated application version available that is
+ * signed with a different key.
+ */
+ private static final int NOT_LICENSED = 0x1;
+ /**
+ * The application is not licensed to the user.
+ */
+ private static final int LICENSED_OLD_KEY = 0x2;
+ /**
+ * Server error — the application (package name) was not recognized by Google Play.
+ */
+ private static final int ERROR_NOT_MARKET_MANAGED = 0x3;
+ /**
+ * Server error — the server could not load the application's key pair for licensing.
+ */
+ private static final int ERROR_SERVER_FAILURE = 0x4;
+ private static final int ERROR_OVER_QUOTA = 0x5;
+
+ /**
+ * Local error — the Google Play application was not able to reach the licensing server, possibly because
+ * of network availability problems.
+ */
+ private static final int ERROR_CONTACTING_SERVER = 0x101;
+ /**
+ * Local error — the application requested a license check for a package that is not installed on the device.
+ */
+ private static final int ERROR_INVALID_PACKAGE_NAME = 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.
+ */
+ private static final int ERROR_NON_MATCHING_UID = 0x103;
+
+ private final ILicensingService.Stub mLicenseService = new ILicensingService.Stub() {
+
+
+ @Override
+ public void checkLicense(long nonce, String packageName, ILicenseResultListener listener) throws RemoteException {
+ Log.v(TAG, "checkLicense(" + nonce + ", " + packageName + ")");
+ try {
+ PackageInfo packageInfo = getPackageManager().getPackageInfo(packageName, 0);
+ int versionCode = packageInfo.versionCode;
+
+ // Verify caller identity
+ if (packageInfo.applicationInfo.uid != getCallingUid()) {
+ Log.e(TAG, "an app illegally tried to request v1 licenses for another app (caller: " + getCallingUid() + ")");
+ listener.verifyLicense(ERROR_NON_MATCHING_UID, null, null);
+ } else {
+
+ Account[] accounts = accountManager.getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE);
+
+ if (accounts.length == 0) {
+ Log.e(TAG, "not checking license, as user is not signed in");
+ } else accountManager.getAuthToken(
+ accounts[0], AUTH_TOKEN_SCOPE, false,
+ future -> {
+ Request request = null;
+ try {
+ request = new LicenseRequest.V1(
+ packageName,
+ future.getResult().getString(KEY_AUTHTOKEN),
+ versionCode, nonce, data -> {
+ if (data != null) {
+ Log.v(TAG, "licenseV1 result was " + data.result + " with signed data " +
+ data.signedData);
+
+ try {
+ if (data.result != null) {
+ listener.verifyLicense(data.result, data.signedData, data.signature);
+ } else {
+ listener.verifyLicense(LICENSED, data.signedData, data.signature);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG,
+ "After telling it the licenseV1 result, remote threw an Exception.");
+ e.printStackTrace();
+ }
+ } else {
+ Log.v(TAG, "licenseV1 result was that user has no license");
+ sendError(listener, NOT_LICENSED);
+ }
+ }, error -> {
+ Log.e(TAG, "licenseV1 request failed with " + error.toString());
+ sendError(listener, ERROR_CONTACTING_SERVER);
+ });
+ } catch (AuthenticatorException | IOException | OperationCanceledException e) {
+ sendError(listener, ERROR_CONTACTING_SERVER);
+ }
+
+ request.setShouldCache(false);
+ queue.add(request);
+ }, null);
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.e(TAG, "an app tried to request v1 licenses for package " + packageName + ", which does not exist");
+ listener.verifyLicense(ERROR_INVALID_PACKAGE_NAME, null, null);
+ }
+ }
+
+ @Override
+ public void checkLicenseV2(String packageName, ILicenseV2ResultListener listener, Bundle extraParams) throws RemoteException {
+ Log.v(TAG, "checkLicenseV2(" + packageName + ", " + extraParams + ")");
+
+ try {
+ PackageInfo packageInfo = getPackageManager().getPackageInfo(packageName, 0);
+ int versionCode = packageInfo.versionCode;
+
+ // Verify caller identity
+ if (packageInfo.applicationInfo.uid != getCallingUid()) {
+ Log.e(TAG, "an app illegally tried to request v2 licenses for another app (caller: " + getCallingUid() + ")");
+ listener.verifyLicense(ERROR_NON_MATCHING_UID, new Bundle());
+ } else {
+ Account[] accounts = accountManager.getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE);
+
+ if (accounts.length == 0) {
+ Log.e(TAG, "not checking license, as user is not signed in");
+ } else accountManager.getAuthToken(
+ accounts[0], AUTH_TOKEN_SCOPE, false,
+ future -> {
+ try {
+ Bundle result = future.getResult(10, TimeUnit.SECONDS);
+ String auth = result.getString(KEY_AUTHTOKEN);
+
+ Request request = new LicenseRequest.V2(packageName, auth, versionCode, jwt -> {
+ Log.v(TAG, "LicenseV2 returned JWT license value " + jwt);
+ Bundle bundle = new Bundle();
+ bundle.putString(KEY_V2_RESULT_JWT, jwt);
+ try {
+ listener.verifyLicense(jwt == null ? NOT_LICENSED : LICENSED, bundle);
+ } catch (RemoteException e) {
+ Log.e(TAG, "After returning licenseV2 result, remote threw an Exception.");
+ e.printStackTrace();
+ }
+ }, error -> {
+ Log.e(TAG, "licenseV2 request failed with " + error.toString());
+ sendError(listener, ERROR_CONTACTING_SERVER);
+ });
+
+ request.setShouldCache(false);
+ queue.add(request);
+
+ } catch (AuthenticatorException | IOException | OperationCanceledException e) {
+ sendError(listener, ERROR_CONTACTING_SERVER);
+ e.printStackTrace();
+ }
+ }, null
+ );
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.e(TAG, "an app tried to request v1 licenses for package " + packageName + ", which does not exist");
+ listener.verifyLicense(ERROR_INVALID_PACKAGE_NAME, new Bundle());
+ }
+
+ }
+ };
+
+ private static void sendError(ILicenseResultListener listener, int error) {
+ try {
+ listener.verifyLicense(error, null, null);
+ } catch (RemoteException e) {
+ Log.e(TAG, "After telling it that licenseV1 had an error (" + error + "), remote threw an Exception.");
+ }
+ }
+
+ private static void sendError(ILicenseV2ResultListener listener, int error) {
+ try {
+ listener.verifyLicense(error, new Bundle());
+ } catch (RemoteException e) {
+ Log.e(TAG, "After telling it that licenseV2 had an error (" + error + "), remote threw an Exception.");
+ }
+ }
+
+ public IBinder onBind(Intent intent) {
+ queue = Volley.newRequestQueue(this);
+ accountManager = AccountManager.get(this);
+
+ return mLicenseService;
+ }
+
+
+}