diff --git a/vending-app/build.gradle b/vending-app/build.gradle
index 24ef7bd3e18af5713fde348a492e3c9ab96cb1fc..d48124a7dc8a6723d17617949eb0b4fc9f21fb9e 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"
@@ -28,6 +30,15 @@ android {
         }
     }
 
+    sourceSets {
+        main {
+            java {
+                srcDirs += "build/generated/source/proto/main/java"
+            }
+        }
+    }
+
+
     buildFeatures {
         aidl = true
     }
@@ -36,10 +47,24 @@ android {
         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 "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 2a78870455d3d40aff844e4b20e6a0accf8289fc..4763d05df783280f99a0dc3f279ecf6b6bfc919a 100644
--- a/vending-app/src/main/AndroidManifest.xml
+++ b/vending-app/src/main/AndroidManifest.xml
@@ -9,6 +9,14 @@
         android:name="com.android.vending.CHECK_LICENSE"
         android:protectionLevel="normal" />
 
+    
+    
+    
+
+    
+
     
             
                 
@@ -55,5 +64,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 0000000000000000000000000000000000000000..0585a28e38481874e9c29503ed349db890f89c74
--- /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 0000000000000000000000000000000000000000..ff9992f7df6b0370b18b54bea6975f18cdc9925c
--- /dev/null
+++ b/vending-app/src/main/java/com/android/vending/licensing/LicenseChecker.java
@@ -0,0 +1,206 @@
+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 licensed to the user, but there is an updated application version available that is
+     * signed with a different key.
+     */
+    static final int NOT_LICENSED = 0x1;
+    /**
+     * The application is not licensed to the user.
+     */
+    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 Request> createRequest(String packageName, String auth, int versionCode, D data,
+                                             BiConsumer then, Response.ErrorListener errorListener);
+
+    public void checkLicense(Account account, AccountManager accountManager,
+                             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);
+                            Request> request = createRequest(packageName, auth,
+                                versionCode, queryData, onRequestFinished, onRequestError);
+                            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 Request 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 Request 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 0000000000000000000000000000000000000000..08d6e28ab3a615318f58ec6659781e3b35fc4ef5
--- /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/LicenseServiceNotificationRunnable.java b/vending-app/src/main/java/com/android/vending/licensing/LicenseServiceNotificationRunnable.java
new file mode 100644
index 0000000000000000000000000000000000000000..7a82341ba2d96e7f129ed76f613e11dc8b9adcef
--- /dev/null
+++ b/vending-app/src/main/java/com/android/vending/licensing/LicenseServiceNotificationRunnable.java
@@ -0,0 +1,153 @@
+package com.android.vending.licensing;
+
+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.os.Build;
+import android.util.Log;
+
+import androidx.annotation.CallSuper;
+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_IMMUTABLE
+        );
+
+        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(context.getString(R.string.license_notification_body))
+            .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);
+        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);
+            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 b479aab6445c0dae70ee4cfc118dc72e1a22b47b..8b58ca22edb0a624d22d553030be29cb93bda5ef 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,134 @@
 
 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.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 java.util.Arrays;
+import java.util.LinkedList;
+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 static final String KEY_V2_RESULT_JWT = "LICENSE_DATA";
+
 
     private final ILicensingService.Stub mLicenseService = new ILicensingService.Stub() {
 
+
         @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 + ")");
+
+            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, 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 + ")");
+
+            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, 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) {
+            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);
+            } 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) {
+        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 0000000000000000000000000000000000000000..da8747c2cdb743c647a10bc83e4eec6666d780fa
--- /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 deviceProductName = 1; // e.g. "OnePlusNord"
+  optional string deviceSoc = 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 deviceName = 5; // e.g. "OnePlusNord"; difference to 1 not yet clear
+  optional uint64 androidId = 6;
+  optional string deviceSignature = 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 0000000000000000000000000000000000000000..238aa263f6c9dd82f07026314d91d089ac28798f
--- /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 0000000000000000000000000000000000000000..8e74fbcf691bec79571cd311c426510f1ace2fd7
--- /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 0000000000000000000000000000000000000000..9459ff8274958b48d4bdc5509693d948b0a44933
--- /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 0000000000000000000000000000000000000000..a1fdf972e19ae2f3feef164c5e555898245f35b5
--- /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 5169119862295a82aee48736d5025bcb8d3c6ccf..07c309f959b96d580a2e56bf408c0998f45439d2 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