Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Licensing service #2069

Merged
merged 24 commits into from
Feb 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
4770884
Licensing service
fynngodau Sep 26, 2023
4485f34
Replace hardcoded auth token with auth token fetched via account
fynngodau Oct 1, 2023
78c9404
Notify is user not signed in to any Google account
fynngodau Oct 3, 2023
305d30e
Don't provide most negative results in V2 licensing
fynngodau Oct 3, 2023
a18c534
Improve license notification icon & sound
fynngodau Oct 3, 2023
b88aace
Query licenses from multiple Google accounts in a row
fynngodau Oct 4, 2023
ac265d8
Dismiss all notifications if user wants to sign in
fynngodau Oct 4, 2023
141843f
Always request `android.permission.GET_ACCOUNTS`
fynngodau Oct 4, 2023
884050c
Target JVM 1.8 instead of 17 in fakestore
fynngodau Oct 4, 2023
a46daa3
Require permission to check license
fynngodau Oct 12, 2023
fff18c7
Apply review
fynngodau Oct 26, 2023
8934281
Check preference from within Licensing service
fynngodau Oct 24, 2023
4fe9bb9
Load device profile data for licensing user agent
fynngodau Nov 11, 2023
b65aaee
Apply review
fynngodau Dec 9, 2023
e84bdce
Use Google androidId for Licensing
jonathanklee Dec 19, 2023
30feaa2
Check preference from within Licensing service
fynngodau Oct 24, 2023
9fbaf2e
Fix swapped docstrings
fynngodau Jan 11, 2024
224f822
Fix Ignore on notification not working
fynngodau Jan 11, 2024
da7bb3d
Enable multidex
fynngodau Jan 11, 2024
92e38d1
Check for `POST_NOTIFICATIONS` permission
fynngodau Jan 22, 2024
46e8aac
Use big text style so that the text is displayed fully
Jan 17, 2024
1632e54
Disable `GetLocales` lint
fynngodau Jan 22, 2024
d4f0b56
Enable postprocessing in vending-app
fynngodau Jan 29, 2024
137a521
Revert "Enable multidex"
fynngodau Jan 29, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 46 additions & 1 deletion vending-app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
*/

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'com.squareup.wire'

android {
namespace "com.android.vending"
Expand All @@ -17,6 +19,25 @@ android {
targetSdkVersion androidTargetSdk
}

buildTypes {
debug {
postprocessing {
removeUnusedCode true
removeUnusedResources true
obfuscate false
optimizeCode true
}
}
release {
postprocessing {
removeUnusedCode true
removeUnusedResources true
obfuscate false
optimizeCode true
}
}
}

flavorDimensions = ['target']
productFlavors {
"default" {
Expand All @@ -34,22 +55,46 @@ android {
}
}

sourceSets {
main {
java {
srcDirs += "build/generated/source/proto/main/java"
}
}
}


buildFeatures {
aidl = true
}

lintOptions {
disable 'MissingTranslation'
disable 'MissingTranslation', 'GetLocales'
}

compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}

kotlinOptions {
jvmTarget = 1.8
}
}

dependencies {
implementation project(':fake-signature')
implementation project(':play-services-auth')
implementation project(':play-services-base-core')

implementation "com.squareup.wire:wire-runtime:$wireVersion"
implementation "com.android.volley:volley:$volleyVersion"
}

wire {
kotlin {
javaInterop = true
}
}

if (file('user.gradle').exists()) {
Expand Down
18 changes: 18 additions & 0 deletions vending-app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@
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" />

<uses-permission android:name="org.microg.gms.permission.READ_SETTINGS" />
<uses-permission android:name="com.google.android.gms.permission.READ_SETTINGS" />

<uses-permission
android:name="android.permission.USE_CREDENTIALS"
android:maxSdkVersion="22" />

<application
android:forceQueryable="true"
android:icon="@mipmap/ic_app"
Expand All @@ -30,6 +41,7 @@

<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" />
Expand All @@ -55,5 +67,11 @@
<category android:name="android.intent.category.INFO" />
</intent-filter>
</activity>

<receiver android:name="com.android.vending.licensing.LicenseServiceNotificationRunnable$IgnoreReceiver"
android:exported="false" />
<receiver android:name="com.android.vending.licensing.LicenseServiceNotificationRunnable$SignInReceiver"
android:exported="false" />

</application>
</manifest>
28 changes: 28 additions & 0 deletions vending-app/src/main/java/com/android/vending/Util.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.android.vending;

import android.util.Log;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.zip.GZIPOutputStream;

public class Util {

private static final String TAG = "FakeStoreUtil";

/**
* From <a href="https://stackoverflow.com/a/46688434/">StackOverflow</a>, CC BY-SA 4.0 by Sergey Frolov, adapted.
*/
public static byte[] encodeGzip(final byte[] input) {

try (final ByteArrayOutputStream byteOutput = new ByteArrayOutputStream();
final GZIPOutputStream gzipOutput = new GZIPOutputStream(byteOutput)) {
gzipOutput.write(input);
gzipOutput.finish();
return byteOutput.toByteArray();
} catch (IOException e) {
Log.e(TAG, "Failed to encode bytes as GZIP");
return new byte[0];
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
package com.android.vending.licensing;

import static android.accounts.AccountManager.KEY_AUTHTOKEN;
import static android.os.Binder.getCallingUid;


import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AuthenticatorException;
import android.accounts.OperationCanceledException;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.RemoteException;
import android.util.Log;

import com.android.vending.V1Container;
import com.android.volley.Request;
import com.android.volley.RequestQueue;
import com.android.volley.Response;

import java.io.IOException;

import kotlin.Unit;

/**
* Performs license check including caller UID verification, using a given account, for which
* an auth token is fetched.
*
* @param <D> Request parameter data value type
* @param <R> Result type
*/
public abstract class LicenseChecker<D, R> {

private static final String 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.
*/
static final int LICENSED = 0x0;
/**
* The application is not licensed to the user.
*/
static final int NOT_LICENSED = 0x1;
/**
* The application is licensed to the user, but there is an updated application version available that is
* signed with a different key.
*/
static final int LICENSED_OLD_KEY = 0x2;
/**
* Server error — the application (package name) was not recognized by Google Play.
*/
static final int ERROR_NOT_MARKET_MANAGED = 0x3;
/**
* Server error — the server could not load the application's key pair for licensing.
*/
static final int ERROR_SERVER_FAILURE = 0x4;
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.
*/
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.
*/
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.
*/
static final int ERROR_NON_MATCHING_UID = 0x103;

static final String AUTH_TOKEN_SCOPE = "oauth2:https://www.googleapis.com/auth/googleplay";

public abstract LicenseRequest<?> createRequest(String packageName, String auth, int versionCode, D data,
BiConsumer<Integer, R> then, Response.ErrorListener errorListener);

public void checkLicense(Account account, AccountManager accountManager, String androidId,
String packageName, PackageManager packageManager,
RequestQueue queue, D queryData,
BiConsumerWithException<Integer, R, RemoteException> onResult)
throws RemoteException {
try {
PackageInfo packageInfo = packageManager.getPackageInfo(packageName, 0);
int versionCode = packageInfo.versionCode;

// Verify caller identity
if (packageInfo.applicationInfo.uid != getCallingUid()) {
Log.e(TAG,
"an app illegally tried to request licenses for another app (caller: " + getCallingUid() + ")");
onResult.accept(ERROR_NON_MATCHING_UID, null);
} else {

BiConsumer<Integer, R> onRequestFinished = (Integer integer, R r) -> {
try {
onResult.accept(integer, r);
} catch (RemoteException e) {
Log.e(TAG,
"After telling it the license check result, remote threw an Exception.");
e.printStackTrace();
}
};

Response.ErrorListener onRequestError = error -> {
Log.e(TAG, "license request failed with " + error.toString());
safeSendResult(onResult, ERROR_CONTACTING_SERVER, null);
};

accountManager.getAuthToken(
account, AUTH_TOKEN_SCOPE, false,
future -> {
try {
String auth = future.getResult().getString(KEY_AUTHTOKEN);
LicenseRequest<?> request = createRequest(packageName, auth,
versionCode, queryData, onRequestFinished, onRequestError);

if (androidId != null) {
request.ANDROID_ID = Long.parseLong(androidId, 16);
}

request.setShouldCache(false);
queue.add(request);
} catch (AuthenticatorException | IOException | OperationCanceledException e) {
safeSendResult(onResult, ERROR_CONTACTING_SERVER, null);
}

}, null);
}
} catch (PackageManager.NameNotFoundException e) {
Log.e(TAG, "an app tried to request licenses for package " + packageName + ", which does not exist");
onResult.accept(ERROR_INVALID_PACKAGE_NAME, null);
}
}

private static <A, B, T extends Exception> void safeSendResult(
BiConsumerWithException<A, B, T> consumerWithException, A a, B b) {
try {
consumerWithException.accept(a, b);
} catch (Exception e) {
Log.e(TAG, "While sending result " + a + ", " + b + ", remote encountered an exception.");
e.printStackTrace();
}
}

// Implementations

public static class V1 extends LicenseChecker<Long, Tuple<String, String>> {

@Override
public LicenseRequest<V1Container> createRequest(String packageName, String auth, int versionCode, Long nonce, BiConsumer<Integer, Tuple<String, String>> then,
Response.ErrorListener errorListener) {
return new LicenseRequest.V1(
packageName, auth, versionCode, nonce, response -> {
if (response != null) {
Log.v(TAG, "licenseV1 result was " + response.result + " with signed data " +
response.signedData);

if (response.result != null) {
then.accept(response.result, new Tuple<>(response.signedData, response.signature));
} else {
then.accept(LICENSED, new Tuple<>(response.signedData, response.signature));
}
}
}, errorListener
);
}
}

public static class V2 extends LicenseChecker<Unit, String> {
@Override
public LicenseRequest<String> createRequest(String packageName, String auth, int versionCode, Unit data,
BiConsumer<Integer, String> then, Response.ErrorListener errorListener) {
return new LicenseRequest.V2(
packageName, auth, versionCode, response -> {
if (response != null) {
then.accept(LICENSED, response);
} else {
then.accept(NOT_LICENSED, null);
}
}, errorListener
);
}
}

// Functional interfaces

interface BiConsumerWithException<A, B, T extends Exception> {
void accept(A a, B b) throws T;
}

interface BiConsumer<A, B> {
void accept(A a, B b);
}

static class Tuple<A, B> {
public final A a;
public final B b;

public Tuple(A a, B b) {
this.a = a;
this.b = b;
}
}
}
Loading
Loading