diff --git a/vending-app/build.gradle b/vending-app/build.gradle
index 38376b97cb..f57b700b52 100644
--- a/vending-app/build.gradle
+++ b/vending-app/build.gradle
@@ -4,6 +4,8 @@
*/
apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+apply plugin: 'com.squareup.wire'
android {
namespace "com.android.vending"
@@ -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" {
@@ -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()) {
diff --git a/vending-app/src/main/AndroidManifest.xml b/vending-app/src/main/AndroidManifest.xml
index 6b255e3b4f..bfc542132b 100644
--- a/vending-app/src/main/AndroidManifest.xml
+++ b/vending-app/src/main/AndroidManifest.xml
@@ -9,6 +9,17 @@
android:name="com.android.vending.CHECK_LICENSE"
android:protectionLevel="normal" />
+
+
+
+
+
+
+
+
+
@@ -55,5 +67,11 @@
+
+
+
+
diff --git a/vending-app/src/main/java/com/android/vending/Util.java b/vending-app/src/main/java/com/android/vending/Util.java
new file mode 100644
index 0000000000..0585a28e38
--- /dev/null
+++ b/vending-app/src/main/java/com/android/vending/Util.java
@@ -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 StackOverflow, 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];
+ }
+ }
+}
diff --git a/vending-app/src/main/java/com/android/vending/licensing/LicenseChecker.java b/vending-app/src/main/java/com/android/vending/licensing/LicenseChecker.java
new file mode 100644
index 0000000000..83e76c2f07
--- /dev/null
+++ b/vending-app/src/main/java/com/android/vending/licensing/LicenseChecker.java
@@ -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 Request parameter data value type
+ * @param Result type
+ */
+public abstract class LicenseChecker {
+
+ 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 then, Response.ErrorListener errorListener);
+
+ public void checkLicense(Account account, AccountManager accountManager, String androidId,
+ String packageName, PackageManager packageManager,
+ RequestQueue queue, D queryData,
+ BiConsumerWithException 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 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 void safeSendResult(
+ BiConsumerWithException 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> {
+
+ @Override
+ public LicenseRequest createRequest(String packageName, String auth, int versionCode, Long nonce, BiConsumer> 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 {
+ @Override
+ public LicenseRequest createRequest(String packageName, String auth, int versionCode, Unit data,
+ BiConsumer 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 {
+ void accept(A a, B b) throws T;
+ }
+
+ interface BiConsumer {
+ void accept(A a, B b);
+ }
+
+ static class Tuple {
+ public final A a;
+ public final B b;
+
+ public Tuple(A a, B b) {
+ this.a = a;
+ this.b = b;
+ }
+ }
+}
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 0000000000..843716f086
--- /dev/null
+++ b/vending-app/src/main/java/com/android/vending/licensing/LicenseRequest.java
@@ -0,0 +1,233 @@
+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 com.google.android.gms.common.BuildConfig;
+
+import org.microg.gms.profile.Build;
+
+import java.io.IOException;
+import java.net.URLEncoder;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.UUID;
+
+import okio.ByteString;
+
+public abstract class LicenseRequest extends Request {
+
+ 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;
+ long ANDROID_ID = 1;
+ private static final String FINSKY_VERSION = "Finsky/37.5.24-29%20%5B0%5D%20%5BPR%5D%20565477504";
+
+ 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;
+ }
+
+ @Override
+ public Map getHeaders() {
+
+ 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(Build.VERSION.SDK_INT)
+ .buildNumber(Build.ID)
+ .androidVersion(Build.VERSION.RELEASE)
+ .unknown(0)
+ .build()
+ )
+ .unknown1(new UnknownByte12.Builder().bytes(new ByteString(new byte[]{}
+ )).build())
+ .unknown2(1)
+ .build()
+ )
+ .userAgent(new UserAgent.Builder()
+ .deviceName(Build.DEVICE)
+ .deviceHardware(Build.HARDWARE)
+ .deviceModelName(Build.MODEL)
+ .finskyVersion(FINSKY_VERSION)
+ .deviceProductName(Build.MODEL)
+ .androidId(ANDROID_ID) // must not be 0
+ .buildFingerprint(Build.FINGERPRINT)
+ .build()
+ )
+ .uuid(new Uuid.Builder()
+ .uuid(UUID.randomUUID().toString())
+ .unknown(2)
+ .build()
+ )
+ .build().encode();
+ String xPsRh = new String(Base64.encode(Util.encodeGzip(header), BASE64_FLAGS));
+
+ Log.v(TAG, "X-PS-RH: " + xPsRh);
+
+ String userAgent = FINSKY_VERSION + " (api=3,versionCode=" + BuildConfig.VERSION_CODE + ",sdk=" + Build.VERSION.SDK +
+ ",device=" + encodeString(Build.DEVICE) + ",hardware=" + encodeString(Build.HARDWARE) + ",product=" + encodeString(Build.PRODUCT) +
+ ",platformVersionRelease=" + encodeString(Build.VERSION.RELEASE) + ",model=" + encodeString(Build.MODEL) + ",buildId=" + encodeString(Build.ID) +
+ ",isWideScreen=" + 0 + ",supportedAbis=" + String.join(";", Build.SUPPORTED_ABIS) + ")";
+ Log.v(TAG, "User-Agent: " + userAgent);
+
+ return Map.of(
+ "X-PS-RH", xPsRh,
+ "User-Agent", userAgent,
+ "Authorization", "Bearer " + auth,
+ "Accept-Language", "en-US",
+ "Connection", "Keep-Alive"
+ );
+ }
+
+ private static String encodeString(String s) {
+ return URLEncoder.encode(s).replace("+", "%20");
+ }
+
+ @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/LicenseServiceNotificationRunnable.java b/vending-app/src/main/java/com/android/vending/licensing/LicenseServiceNotificationRunnable.java
new file mode 100644
index 0000000000..84a17dacc3
--- /dev/null
+++ b/vending-app/src/main/java/com/android/vending/licensing/LicenseServiceNotificationRunnable.java
@@ -0,0 +1,164 @@
+package com.android.vending.licensing;
+
+import android.Manifest;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.os.Build;
+import android.util.Log;
+
+import androidx.core.app.ActivityCompat;
+import androidx.core.app.NotificationCompat;
+import androidx.core.app.NotificationManagerCompat;
+
+import com.android.vending.R;
+
+import java.util.Collections;
+import java.util.Set;
+import java.util.TreeSet;
+
+public class LicenseServiceNotificationRunnable implements Runnable {
+
+ private final Context context;
+
+ public String callerPackageName;
+ public CharSequence callerAppName;
+ public int callerUid;
+
+ private static final String TAG = "FakeLicenseNotification";
+ private static final String GMS_PACKAGE_NAME = "com.google.android.gms";
+ private static final String GMS_AUTH_INTENT_ACTION = "com.google.android.gms.auth.login.LOGIN";
+
+ private static final String PREFERENCES_KEY_IGNORE_PACKAGES_LIST = "ignorePackages";
+ private static final String PREFERENCES_FILE_NAME = "licensing";
+
+ private static final String INTENT_KEY_IGNORE_PACKAGE_NAME = "package";
+ private static final String INTENT_KEY_NOTIFICATION_ID = "id";
+
+
+ public LicenseServiceNotificationRunnable(Context context) {
+ this.context = context;
+ }
+
+ private static final String CHANNEL_ID = "LicenseNotification";
+
+ @Override
+ public void run() {
+ registerNotificationChannel();
+
+ SharedPreferences preferences = context.getSharedPreferences(PREFERENCES_FILE_NAME, Context.MODE_PRIVATE);
+
+ Set ignoreList = preferences.getStringSet(PREFERENCES_KEY_IGNORE_PACKAGES_LIST, Collections.emptySet());
+ for (String ignoredPackage : ignoreList) {
+ if (callerPackageName.equals(ignoredPackage)) {
+ Log.d(TAG, "Not notifying about license check, as user has ignored notifications for package " + ignoredPackage);
+ return;
+ }
+ }
+
+ Intent authIntent = new Intent(context, LicenseServiceNotificationRunnable.SignInReceiver.class);
+ authIntent.putExtra(INTENT_KEY_NOTIFICATION_ID, callerUid);
+ PendingIntent authPendingIntent = PendingIntent.getBroadcast(
+ context, callerUid * 2, authIntent, PendingIntent.FLAG_IMMUTABLE
+ );
+
+ Intent ignoreIntent = new Intent(context, LicenseServiceNotificationRunnable.IgnoreReceiver.class);
+ ignoreIntent.putExtra(INTENT_KEY_IGNORE_PACKAGE_NAME, callerPackageName);
+ ignoreIntent.putExtra(INTENT_KEY_NOTIFICATION_ID, callerUid);
+ PendingIntent ignorePendingIntent = PendingIntent.getBroadcast(
+ context, callerUid * 2 + 1, ignoreIntent, PendingIntent.FLAG_MUTABLE
+ );
+
+ String contentText = context.getString(R.string.license_notification_body);
+ Notification notification = new NotificationCompat.Builder(context, CHANNEL_ID)
+ .setSmallIcon(R.drawable.ic_notification)
+ .setSound(null)
+ .setContentTitle(context.getString(R.string.license_notification_title, callerAppName))
+ .setContentText(contentText)
+ .setStyle(new NotificationCompat.BigTextStyle().bigText(contentText))
+ .addAction(
+ new NotificationCompat.Action.Builder(
+ null, context.getString(R.string.license_notification_sign_in), authPendingIntent
+ ).build()
+ )
+ .addAction(
+ new NotificationCompat.Action.Builder(
+ null, context.getString(R.string.license_notification_ignore), ignorePendingIntent
+ ).build()
+ )
+ .setAutoCancel(true)
+ .build();
+
+ NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
+ if (ActivityCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) ==
+ PackageManager.PERMISSION_GRANTED) {
+ notificationManager.notify(callerUid, notification);
+ }
+ }
+
+ private void registerNotificationChannel() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ NotificationChannel channel = new NotificationChannel(
+ CHANNEL_ID,
+ context.getString(R.string.license_notification_channel_name),
+ NotificationManager.IMPORTANCE_HIGH
+ );
+ channel.setDescription(context.getString(R.string.license_notification_channel_description));
+ channel.setSound(null, null);
+
+ NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
+ notificationManager.createNotificationChannel(channel);
+ }
+
+ }
+
+ public static final class IgnoreReceiver extends BroadcastReceiver {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+
+ // Dismiss ignored notification
+ NotificationManagerCompat.from(context)
+ .cancel(intent.getIntExtra(INTENT_KEY_NOTIFICATION_ID, -1));
+
+ SharedPreferences preferences = context.getSharedPreferences(PREFERENCES_FILE_NAME, Context.MODE_PRIVATE);
+
+ Set ignoreList = new TreeSet<>(
+ preferences.getStringSet(PREFERENCES_KEY_IGNORE_PACKAGES_LIST, Collections.emptySet())
+ );
+
+ String 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();
+ }
+ }
+
+ public static final class SignInReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+
+ // Dismiss all notifications
+ NotificationManagerCompat.from(context).cancelAll();
+
+ Log.d(TAG, "Starting sign in activity");
+ Intent authIntent = new Intent(GMS_AUTH_INTENT_ACTION);
+ authIntent.setPackage(GMS_PACKAGE_NAME);
+ authIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ context.startActivity(authIntent);
+ }
+ }
+
+}
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
index b479aab644..c66e9b7274 100644
--- a/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java
+++ b/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java
@@ -5,32 +5,227 @@
package com.android.vending.licensing;
+import static com.android.vending.licensing.LicenseChecker.LICENSED;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
import android.app.Service;
import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.net.Uri;
+import android.net.Uri;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
+import com.android.volley.RequestQueue;
+import com.android.volley.toolbox.Volley;
+
+import org.microg.gms.auth.AuthConstants;
+import org.microg.gms.profile.ProfileManager;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.Queue;
+
+import kotlin.Unit;
+
public class LicensingService extends Service {
private static final String TAG = "FakeLicenseService";
+ private RequestQueue queue;
+ private AccountManager accountManager;
+ private LicenseServiceNotificationRunnable notificationRunnable;
+ private String androidId;
+
+ private static final String KEY_V2_RESULT_JWT = "LICENSE_DATA";
+
+ private static final Uri PROFILE_PROVIDER = Uri.parse("content://com.google.android.gms.microg.profile");
+
+ private static final Uri CHECKIN_SETTINGS_PROVIDER = Uri.parse("content://com.google.android.gms.microg.settings/check-in");
+
+ private static final Uri SETTINGS_PROVIDER = Uri.parse("content://com.google.android.gms.microg.settings/vending");
+
+ private static final String PREFERENCE_LICENSING_ENABLED = "vending_licensing";
private final ILicensingService.Stub mLicenseService = new ILicensingService.Stub() {
+ private boolean shouldCheckLicense() {
+
+ Cursor cursor = null;
+ try {
+ cursor = getContentResolver().query(
+ SETTINGS_PROVIDER, new String[]{PREFERENCE_LICENSING_ENABLED}, null, null, null
+ );
+
+ if (cursor == null || cursor.getColumnCount() != 1) {
+ Log.e(TAG, "settings provider not available");
+ return false;
+ } else {
+ cursor.moveToNext();
+ return cursor.getInt(0) != 0;
+ }
+
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ }
+
+
@Override
public void checkLicense(long nonce, String packageName, ILicenseResultListener listener) throws RemoteException {
- Log.d(TAG, "checkLicense(" + nonce + ", " + packageName + ")");
- // We don't return anything yet. Seems to work good for some checkers.
+ Log.v(TAG, "checkLicense(" + nonce + ", " + packageName + ")");
+
+ if (!shouldCheckLicense()) {
+ Log.d(TAG, "not checking license, as it is disabled by user");
+ return;
+ }
+
+ Account[] accounts = accountManager.getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE);
+ PackageManager packageManager = getPackageManager();
+
+ if (accounts.length == 0) {
+ handleNoAccounts(packageName, packageManager);
+ } else {
+ checkLicense(nonce, packageName, packageManager, listener, new LinkedList<>(Arrays.asList(accounts)));
+ }
+ }
+
+ private void checkLicense(long nonce, String packageName, PackageManager packageManager,
+ ILicenseResultListener listener, Queue remainingAccounts) throws RemoteException {
+ new LicenseChecker.V1().checkLicense(
+ remainingAccounts.poll(), accountManager, androidId, packageName, packageManager,
+ queue, nonce,
+ (responseCode, stringTuple) -> {
+ if (responseCode != LICENSED && !remainingAccounts.isEmpty()) {
+ checkLicense(nonce, packageName, packageManager, listener, remainingAccounts);
+ } else {
+ listener.verifyLicense(responseCode, stringTuple.a, stringTuple.b);
+ }
+ }
+ );
}
@Override
public void checkLicenseV2(String packageName, ILicenseV2ResultListener listener, Bundle extraParams) throws RemoteException {
- Log.d(TAG, "checkLicenseV2(" + packageName + ", " + extraParams + ")");
- // We don't return anything yet. Seems to work good for some checkers.
+ Log.v(TAG, "checkLicenseV2(" + packageName + ", " + extraParams + ")");
+
+ if (!shouldCheckLicense()) {
+ Log.d(TAG, "not checking license, as it is disabled by user");
+ return;
+ }
+
+ Account[] accounts = accountManager.getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE);
+ PackageManager packageManager = getPackageManager();
+
+ if (accounts.length == 0) {
+ handleNoAccounts(packageName, packageManager);
+ } else {
+ checkLicenseV2(packageName, packageManager, listener, extraParams, new LinkedList<>(Arrays.asList(accounts)));
+ }
+ }
+
+ private void checkLicenseV2(String packageName, PackageManager packageManager,
+ ILicenseV2ResultListener listener, Bundle extraParams,
+ Queue remainingAccounts) throws RemoteException {
+ new LicenseChecker.V2().checkLicense(
+ remainingAccounts.poll(), accountManager, androidId, packageName, packageManager, queue, Unit.INSTANCE,
+ (responseCode, data) -> {
+ /*
+ * 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.
+ */
+ if (responseCode == LICENSED) {
+ Bundle bundle = new Bundle();
+ bundle.putString(KEY_V2_RESULT_JWT, data);
+
+ listener.verifyLicense(responseCode, bundle);
+ } else if (!remainingAccounts.isEmpty()) {
+ checkLicenseV2(packageName, packageManager, listener, extraParams, remainingAccounts);
+ } else {
+ Log.i(TAG, "Suppressed negative license result for package " + packageName);
+ }
+ }
+ );
+
+ }
+
+ private void handleNoAccounts(String packageName, PackageManager packageManager) {
+ notificationRunnable.callerPackageName = packageName;
+ try {
+ Log.e(TAG, "not checking license, as user is not signed in");
+ PackageInfo packageInfo = packageManager.getPackageInfo(packageName, 0);
+ notificationRunnable.callerUid = packageInfo.applicationInfo.uid;
+ notificationRunnable.callerAppName = packageManager.getApplicationLabel(packageInfo.applicationInfo);
+ if (notificationRunnable.callerAppName == null) {
+ notificationRunnable.callerAppName = packageName;
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.e(TAG, "ignored license request, but package name " + packageName + " was not known!");
+ notificationRunnable.callerAppName = packageName;
+ }
+ notificationRunnable.run();
}
};
public IBinder onBind(Intent intent) {
+
+
+ Cursor cursor = null;
+ try {
+ cursor = getContentResolver().query(
+ PROFILE_PROVIDER, null, null, null, null
+ );
+
+ if (cursor == null || cursor.getColumnCount() != 2) {
+ Log.e(TAG, "profile provider not available");
+ } else {
+ Map profileData = new HashMap<>();
+ while (cursor.moveToNext()) {
+ profileData.put(cursor.getString(0), cursor.getString(1));
+ }
+ ProfileManager.INSTANCE.applyProfileData(profileData);
+ }
+
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+
+ try {
+ cursor = getContentResolver().query(
+ CHECKIN_SETTINGS_PROVIDER, new String[] { "androidId" }, null, null, null
+ );
+
+ if (cursor != null) {
+ cursor.moveToNext();
+ androidId = Long.toHexString(cursor.getLong(0));
+ }
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+
+ queue = Volley.newRequestQueue(this);
+ accountManager = AccountManager.get(this);
+ notificationRunnable = new LicenseServiceNotificationRunnable(this);
+
return mLicenseService;
}
+
+
}
diff --git a/vending-app/src/main/proto/LicenseRequest.proto b/vending-app/src/main/proto/LicenseRequest.proto
new file mode 100644
index 0000000000..dc0b71339e
--- /dev/null
+++ b/vending-app/src/main/proto/LicenseRequest.proto
@@ -0,0 +1,71 @@
+syntax = "proto2";
+
+option java_package = "com.android.vending";
+option java_multiple_files = true;
+
+message LicenseRequestHeader {
+ optional StringWrapper encodedTimestamps = 1;
+ optional EncodedTripleWrapper triple = 10;
+ optional LocalityWrapper locality = 11;
+ optional IntWrapper unknown = 12;
+ optional string empty = 14;
+ optional DeviceMeta deviceMeta = 20;
+ optional UserAgent userAgent = 21;
+ optional Uuid uuid = 27;
+}
+
+message StringWrapper {
+ optional string string = 1;
+}
+
+message EncodedTripleWrapper {
+ optional EncodedTriple triple = 1;
+}
+
+message EncodedTriple {
+ optional string encoded1 = 1;
+ optional string encoded2 = 2;
+ optional string empty = 3;
+}
+
+message LocalityWrapper {
+ optional string encodedLocalityProto = 1;
+}
+
+message IntWrapper {
+ optional uint32 integer = 1;
+}
+
+message DeviceMeta {
+ optional AndroidVersionMeta android = 1;
+ optional UnknownByte12 unknown1 = 2;
+ optional uint32 unknown2 = 3; // observed value: 1
+
+}
+
+message AndroidVersionMeta {
+ optional uint32 androidSdk = 1;
+ optional string buildNumber = 2;
+ optional string androidVersion = 3;
+ optional uint32 unknown = 4;
+}
+
+message UnknownByte12 {
+ optional bytes bytes = 12;
+}
+
+message UserAgent {
+ // The names of these attributes are vague guesses and should be adapted if needed.
+ optional string deviceName = 1; // e.g. "OnePlusNord"
+ optional string deviceHardware = 2; // e.g. "qcom"
+ optional string deviceModelName = 3; // e.g. "OnePlus Nord"
+ optional string finskyVersion = 4; // e.g. "Finsky/37.5.24-29%20%5B0%5D%20%5BPR%5D%20565477504"
+ optional string deviceProductName = 5; // e.g. "OnePlusNord"
+ optional uint64 androidId = 6;
+ optional string buildFingerprint = 7; // e.g. "google/walleye/walleye:8.1.0/OPM1.171019.011/4448085:user/release-keys"
+}
+
+message Uuid {
+ optional string uuid = 1;
+ optional uint32 unknown = 2; // might be a constant, e.g. format ID. Observed value: 2.
+}
diff --git a/vending-app/src/main/proto/LicenseResult.proto b/vending-app/src/main/proto/LicenseResult.proto
new file mode 100644
index 0000000000..238aa263f6
--- /dev/null
+++ b/vending-app/src/main/proto/LicenseResult.proto
@@ -0,0 +1,28 @@
+syntax = "proto2";
+
+option java_package = "com.android.vending";
+option java_multiple_files = true;
+
+message LicenseResult {
+ optional LicenseInformation information = 1;
+}
+
+message LicenseInformation {
+ optional V1Container v1 = 76;
+ optional V2Container v2 = 173;
+}
+
+message V1Container {
+ optional uint32 result = 1;
+ optional string signedData = 2;
+ optional string signature = 3;
+
+}
+
+message V2Container {
+ optional AppLicense license = 1;
+}
+
+message AppLicense {
+ optional string jwt = 1;
+}
\ No newline at end of file
diff --git a/vending-app/src/main/proto/Locality.proto b/vending-app/src/main/proto/Locality.proto
new file mode 100644
index 0000000000..8e74fbcf69
--- /dev/null
+++ b/vending-app/src/main/proto/Locality.proto
@@ -0,0 +1,21 @@
+syntax = "proto2";
+
+option java_package = "com.android.vending";
+option java_multiple_files = true;
+
+import "Timestamp.proto";
+
+message Locality {
+ optional uint32 unknown1 = 2; // value: 1
+ optional uint32 unknown2 = 3; // value: 0
+ optional string countryCode = 4; // e.g. "DE"
+ optional TimestampStringWrapper region = 8; // e.g. "DE-BY" and a timestamp
+ optional TimestampStringWrapper country = 9; // e.g. "DE" and a timestamp
+ optional uint32 unknown3 = 11; // value: 0
+
+}
+
+message TimestampStringWrapper {
+ optional string string = 1;
+ optional Timestamp timestamp = 2;
+}
diff --git a/vending-app/src/main/proto/Timestamp.proto b/vending-app/src/main/proto/Timestamp.proto
new file mode 100644
index 0000000000..9459ff8274
--- /dev/null
+++ b/vending-app/src/main/proto/Timestamp.proto
@@ -0,0 +1,33 @@
+syntax = "proto2";
+
+option java_package = "com.android.vending";
+option java_multiple_files = true;
+
+message TimestampContainer {
+ optional TimestampContainer1Wrapper container1Wrapper = 3;
+ optional TimestampContainer2 container2 = 6;
+}
+
+message TimestampContainer1Wrapper {
+ optional string androidId = 1;
+ optional TimestampContainer1 container = 2;
+}
+
+message TimestampContainer1 {
+ optional string timestamp = 1;
+ optional Timestamp wrapper = 2;
+}
+
+message Timestamp {
+ optional uint32 seconds = 1;
+ optional uint32 nanos = 2;
+}
+
+message TimestampContainer2 {
+ optional TimestampWrapper wrapper = 1;
+ optional Timestamp timestamp = 2;
+}
+
+message TimestampWrapper {
+ optional Timestamp timestamp = 1;
+}
\ No newline at end of file
diff --git a/vending-app/src/main/res/drawable/ic_notification.xml b/vending-app/src/main/res/drawable/ic_notification.xml
new file mode 100644
index 0000000000..a1fdf972e1
--- /dev/null
+++ b/vending-app/src/main/res/drawable/ic_notification.xml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vending-app/src/main/res/values/strings.xml b/vending-app/src/main/res/values/strings.xml
index 5169119862..07c309f959 100644
--- a/vending-app/src/main/res/values/strings.xml
+++ b/vending-app/src/main/res/values/strings.xml
@@ -8,4 +8,12 @@
microG Companion
microG Companion cannot be used standalone. Opened microG Services settings instead.
microG Companion cannot be used standalone. Please install microG Services to use microG.
+
+ License notifications
+ Notifies when an app tries to validate its license, but you are not signed in to any Google account.
+ %1$s could not verify license
+ If the app is misbehaving, sign in to a Google account with which you have bought the app.
+
+ Sign In
+ Ignore
\ No newline at end of file