Commits

Fred Grott committed 49ffe61

first commit

  • Participants

Comments (0)

Files changed (42)

+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+	<classpathentry kind="src" path="src"/>
+	<classpathentry kind="src" path="gen"/>
+	<classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
+	<classpathentry kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
+	<classpathentry kind="output" path="bin/classes"/>
+</classpath>
+bin/*
+gen/*
+local.properties
+ext-libs/*
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+	<name>GWSPlayLicensing</name>
+	<comment></comment>
+	<projects>
+	</projects>
+	<buildSpec>
+		<buildCommand>
+			<name>com.android.ide.eclipse.adt.ResourceManagerBuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+		<buildCommand>
+			<name>com.android.ide.eclipse.adt.PreCompilerBuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+		<buildCommand>
+			<name>org.eclipse.jdt.core.javabuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+		<buildCommand>
+			<name>com.android.ide.eclipse.adt.ApkBuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+	</buildSpec>
+	<natures>
+		<nature>com.android.ide.eclipse.adt.AndroidNature</nature>
+		<nature>org.eclipse.jdt.core.javanature</nature>
+	</natures>
+</projectDescription>

.settings/org.eclipse.jdt.core.prefs

+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.5
+org.eclipse.jdt.core.compiler.compliance=1.5
+org.eclipse.jdt.core.compiler.source=1.5

AndroidManifest.xml

+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="org.bitbucket.fredgrott.gwsplaylicensing"
+    android:versionCode="1"
+    android:versionName="1.0">
+
+    <uses-sdk android:minSdkVersion="11" android:targetSdkVersion="15" />
+
+    <application android:label="@string/app_name"
+        android:icon="@drawable/ic_launcher"
+        android:theme="@style/AppTheme">
+
+    </application>
+
+</manifest>

aidl/ILicenseResultListener.aidl

+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.vending.licensing;
+
+// Android library projects do not yet support AIDL, so this has been
+// precompiled into the src directory.
+oneway interface ILicenseResultListener {
+  void verifyLicense(int responseCode, String signedData, String signature);
+}

aidl/ILicensingService.aidl

+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.vending.licensing;
+
+import com.android.vending.licensing.ILicenseResultListener;
+
+// Android library projects do not yet support AIDL, so this has been
+// precompiled into the src directory.
+oneway interface ILicensingService {
+  void checkLicense(long nonce, String packageName, in ILicenseResultListener listener);
+}
+source.dir=src
+out.dir=bin/classes

libs/android-support-v4.jar

Binary file added.

proguard-project.txt

+# To enable ProGuard in your project, edit project.properties
+# to define the proguard.config property as described in that file.
+#
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in ${sdk.dir}/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the ProGuard
+# include property in project.properties.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}

project.properties

+# This file is automatically generated by Android Tools.
+# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
+#
+# This file must be checked in Version Control Systems.
+#
+# To customize properties used by the Ant build system edit
+# "ant.properties", and override values to adapt the script to your
+# project structure.
+#
+# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
+#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
+
+# Project target.
+target=android-16
+android.library=true
+GWSPlayLicensing
+---
+
+Android Project Library for Play Licensing.
+
+# Implementatin Notes
+
+APLs do not yet supoprt AIDLs(maybe android tools 21?), thus Google precompiled 
+the aidls required and than posted the code generated for those aidls within the src 
+of the play licensing extras in the SDK.
+
+Copy of the original aidl files is in the aidl folder. My min target is se tto 11 if you need 
+a lower min target fork this repo and change it for your use.
+
+# License
+
+Apache License 2.0
+
+# Credits
+
+Android Open Source Project

res/drawable-hdpi/ic_action_search.png

Added
New image

res/drawable-hdpi/ic_launcher.png

Added
New image

res/drawable-mdpi/ic_action_search.png

Added
New image

res/drawable-mdpi/ic_launcher.png

Added
New image

res/drawable-xhdpi/ic_action_search.png

Added
New image

res/drawable-xhdpi/ic_launcher.png

Added
New image

res/values-v11/styles.xml

+<resources>
+
+    <style name="AppTheme" parent="android:Theme.Holo.Light" />
+
+</resources>

res/values-v14/styles.xml

+<resources>
+
+    <style name="AppTheme" parent="android:Theme.Holo.Light.DarkActionBar" />
+
+</resources>

res/values/strings.xml

+<resources>
+    <string name="app_name">GWSPlayLicensing</string>
+</resources>

res/values/styles.xml

+<resources>
+
+    <style name="AppTheme" parent="android:Theme.Light" />
+
+</resources>

src/com/google/android/vending/licensing/AESObfuscator.java

+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.vending.licensing;
+
+import com.google.android.vending.licensing.util.Base64;
+import com.google.android.vending.licensing.util.Base64DecoderException;
+
+import java.io.UnsupportedEncodingException;
+import java.security.GeneralSecurityException;
+import java.security.spec.KeySpec;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.SecretKey;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.PBEKeySpec;
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * An Obfuscator that uses AES to encrypt data.
+ */
+public class AESObfuscator implements Obfuscator {
+    private static final String UTF8 = "UTF-8";
+    private static final String KEYGEN_ALGORITHM = "PBEWITHSHAAND256BITAES-CBC-BC";
+    private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding";
+    private static final byte[] IV =
+        { 16, 74, 71, -80, 32, 101, -47, 72, 117, -14, 0, -29, 70, 65, -12, 74 };
+    private static final String header = "com.android.vending.licensing.AESObfuscator-1|";
+
+    private Cipher mEncryptor;
+    private Cipher mDecryptor;
+
+    /**
+     * @param salt an array of random bytes to use for each (un)obfuscation
+     * @param applicationId application identifier, e.g. the package name
+     * @param deviceId device identifier. Use as many sources as possible to
+     *    create this unique identifier.
+     */
+    public AESObfuscator(byte[] salt, String applicationId, String deviceId) {
+        try {
+            SecretKeyFactory factory = SecretKeyFactory.getInstance(KEYGEN_ALGORITHM);
+            KeySpec keySpec =   
+                new PBEKeySpec((applicationId + deviceId).toCharArray(), salt, 1024, 256);
+            SecretKey tmp = factory.generateSecret(keySpec);
+            SecretKey secret = new SecretKeySpec(tmp.getEncoded(), "AES");
+            mEncryptor = Cipher.getInstance(CIPHER_ALGORITHM);
+            mEncryptor.init(Cipher.ENCRYPT_MODE, secret, new IvParameterSpec(IV));
+            mDecryptor = Cipher.getInstance(CIPHER_ALGORITHM);
+            mDecryptor.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec(IV));
+        } catch (GeneralSecurityException e) {
+            // This can't happen on a compatible Android device.
+            throw new RuntimeException("Invalid environment", e);
+        }
+    }
+
+    public String obfuscate(String original, String key) {
+        if (original == null) {
+            return null;
+        }
+        try {
+            // Header is appended as an integrity check
+            return Base64.encode(mEncryptor.doFinal((header + key + original).getBytes(UTF8)));
+        } catch (UnsupportedEncodingException e) {
+            throw new RuntimeException("Invalid environment", e);
+        } catch (GeneralSecurityException e) {
+            throw new RuntimeException("Invalid environment", e);
+        }
+    }
+
+    public String unobfuscate(String obfuscated, String key) throws ValidationException {
+        if (obfuscated == null) {
+            return null;
+        }
+        try {
+            String result = new String(mDecryptor.doFinal(Base64.decode(obfuscated)), UTF8);
+            // Check for presence of header. This serves as a final integrity check, for cases
+            // where the block size is correct during decryption.
+            int headerIndex = result.indexOf(header+key);
+            if (headerIndex != 0) {
+                throw new ValidationException("Header not found (invalid data or key)" + ":" +
+                        obfuscated);
+            }
+            return result.substring(header.length()+key.length(), result.length());
+        } catch (Base64DecoderException e) {
+            throw new ValidationException(e.getMessage() + ":" + obfuscated);
+        } catch (IllegalBlockSizeException e) {
+            throw new ValidationException(e.getMessage() + ":" + obfuscated);
+        } catch (BadPaddingException e) {
+            throw new ValidationException(e.getMessage() + ":" + obfuscated);
+        } catch (UnsupportedEncodingException e) {
+            throw new RuntimeException("Invalid environment", e);
+        }
+    }
+}

src/com/google/android/vending/licensing/APKExpansionPolicy.java

+package com.google.android.vending.licensing;
+
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import org.apache.http.NameValuePair;
+import org.apache.http.client.utils.URLEncodedUtils;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.util.Log;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.Vector;
+
+/**
+ * Default policy. All policy decisions are based off of response data received
+ * from the licensing service. Specifically, the licensing server sends the
+ * following information: response validity period, error retry period, and
+ * error retry count.
+ * <p>
+ * These values will vary based on the the way the application is configured in
+ * the Android Market publishing console, such as whether the application is
+ * marked as free or is within its refund period, as well as how often an
+ * application is checking with the licensing service.
+ * <p>
+ * Developers who need more fine grained control over their application's
+ * licensing policy should implement a custom Policy.
+ */
+public class APKExpansionPolicy implements Policy {
+
+    private static final String TAG = "APKExpansionPolicy";
+    private static final String PREFS_FILE = "com.android.vending.licensing.APKExpansionPolicy";
+    private static final String PREF_LAST_RESPONSE = "lastResponse";
+    private static final String PREF_VALIDITY_TIMESTAMP = "validityTimestamp";
+    private static final String PREF_RETRY_UNTIL = "retryUntil";
+    private static final String PREF_MAX_RETRIES = "maxRetries";
+    private static final String PREF_RETRY_COUNT = "retryCount";
+    private static final String DEFAULT_VALIDITY_TIMESTAMP = "0";
+    private static final String DEFAULT_RETRY_UNTIL = "0";
+    private static final String DEFAULT_MAX_RETRIES = "0";
+    private static final String DEFAULT_RETRY_COUNT = "0";
+
+    private static final long MILLIS_PER_MINUTE = 60 * 1000;
+
+    private long mValidityTimestamp;
+    private long mRetryUntil;
+    private long mMaxRetries;
+    private long mRetryCount;
+    private long mLastResponseTime = 0;
+    private int mLastResponse;
+    private PreferenceObfuscator mPreferences;
+    private Vector<String> mExpansionURLs = new Vector<String>();
+    private Vector<String> mExpansionFileNames = new Vector<String>();
+    private Vector<Long> mExpansionFileSizes = new Vector<Long>();
+
+    /**
+     * The design of the protocol supports n files. Currently the market can
+     * only deliver two files. To accommodate this, we have these two constants,
+     * but the order is the only relevant thing here.
+     */
+    public static final int MAIN_FILE_URL_INDEX = 0;
+    public static final int PATCH_FILE_URL_INDEX = 1;
+
+    /**
+     * @param context The context for the current application
+     * @param obfuscator An obfuscator to be used with preferences.
+     */
+    public APKExpansionPolicy(Context context, Obfuscator obfuscator) {
+        // Import old values
+        SharedPreferences sp = context.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE);
+        mPreferences = new PreferenceObfuscator(sp, obfuscator);
+        mLastResponse = Integer.parseInt(
+                mPreferences.getString(PREF_LAST_RESPONSE, Integer.toString(Policy.RETRY)));
+        mValidityTimestamp = Long.parseLong(mPreferences.getString(PREF_VALIDITY_TIMESTAMP,
+                DEFAULT_VALIDITY_TIMESTAMP));
+        mRetryUntil = Long.parseLong(mPreferences.getString(PREF_RETRY_UNTIL, DEFAULT_RETRY_UNTIL));
+        mMaxRetries = Long.parseLong(mPreferences.getString(PREF_MAX_RETRIES, DEFAULT_MAX_RETRIES));
+        mRetryCount = Long.parseLong(mPreferences.getString(PREF_RETRY_COUNT, DEFAULT_RETRY_COUNT));
+    }
+
+    /**
+     * We call this to guarantee that we fetch a fresh policy from the server.
+     * This is to be used if the URL is invalid.
+     */
+    public void resetPolicy() {
+        mPreferences.putString(PREF_LAST_RESPONSE, Integer.toString(Policy.RETRY));
+        setRetryUntil(DEFAULT_RETRY_UNTIL);
+        setMaxRetries(DEFAULT_MAX_RETRIES);
+        setRetryCount(Long.parseLong(DEFAULT_RETRY_COUNT));
+        setValidityTimestamp(DEFAULT_VALIDITY_TIMESTAMP);
+        mPreferences.commit();
+    }
+
+    /**
+     * Process a new response from the license server.
+     * <p>
+     * This data will be used for computing future policy decisions. The
+     * following parameters are processed:
+     * <ul>
+     * <li>VT: the timestamp that the client should consider the response valid
+     * until
+     * <li>GT: the timestamp that the client should ignore retry errors until
+     * <li>GR: the number of retry errors that the client should ignore
+     * </ul>
+     * 
+     * @param response the result from validating the server response
+     * @param rawData the raw server response data
+     */
+    public void processServerResponse(int response,
+            com.google.android.vending.licensing.ResponseData rawData) {
+
+        // Update retry counter
+        if (response != Policy.RETRY) {
+            setRetryCount(0);
+        } else {
+            setRetryCount(mRetryCount + 1);
+        }
+
+        if (response == Policy.LICENSED) {
+            // Update server policy data
+            Map<String, String> extras = decodeExtras(rawData.extra);
+            mLastResponse = response;
+            setValidityTimestamp(Long.toString(System.currentTimeMillis() + MILLIS_PER_MINUTE));
+            Set<String> keys = extras.keySet();
+            for (String key : keys) {
+                if (key.equals("VT")) {
+                    setValidityTimestamp(extras.get(key));
+                } else if (key.equals("GT")) {
+                    setRetryUntil(extras.get(key));
+                } else if (key.equals("GR")) {
+                    setMaxRetries(extras.get(key));
+                } else if (key.startsWith("FILE_URL")) {
+                    int index = Integer.parseInt(key.substring("FILE_URL".length())) - 1;
+                    setExpansionURL(index, extras.get(key));
+                } else if (key.startsWith("FILE_NAME")) {
+                    int index = Integer.parseInt(key.substring("FILE_NAME".length())) - 1;
+                    setExpansionFileName(index, extras.get(key));
+                } else if (key.startsWith("FILE_SIZE")) {
+                    int index = Integer.parseInt(key.substring("FILE_SIZE".length())) - 1;
+                    setExpansionFileSize(index, Long.parseLong(extras.get(key)));
+                }
+            }
+        } else if (response == Policy.NOT_LICENSED) {
+            // Clear out stale policy data
+            setValidityTimestamp(DEFAULT_VALIDITY_TIMESTAMP);
+            setRetryUntil(DEFAULT_RETRY_UNTIL);
+            setMaxRetries(DEFAULT_MAX_RETRIES);
+        }
+
+        setLastResponse(response);
+        mPreferences.commit();
+    }
+
+    /**
+     * Set the last license response received from the server and add to
+     * preferences. You must manually call PreferenceObfuscator.commit() to
+     * commit these changes to disk.
+     * 
+     * @param l the response
+     */
+    private void setLastResponse(int l) {
+        mLastResponseTime = System.currentTimeMillis();
+        mLastResponse = l;
+        mPreferences.putString(PREF_LAST_RESPONSE, Integer.toString(l));
+    }
+
+    /**
+     * Set the current retry count and add to preferences. You must manually
+     * call PreferenceObfuscator.commit() to commit these changes to disk.
+     * 
+     * @param c the new retry count
+     */
+    private void setRetryCount(long c) {
+        mRetryCount = c;
+        mPreferences.putString(PREF_RETRY_COUNT, Long.toString(c));
+    }
+
+    public long getRetryCount() {
+        return mRetryCount;
+    }
+
+    /**
+     * Set the last validity timestamp (VT) received from the server and add to
+     * preferences. You must manually call PreferenceObfuscator.commit() to
+     * commit these changes to disk.
+     * 
+     * @param validityTimestamp the VT string received
+     */
+    private void setValidityTimestamp(String validityTimestamp) {
+        Long lValidityTimestamp;
+        try {
+            lValidityTimestamp = Long.parseLong(validityTimestamp);
+        } catch (NumberFormatException e) {
+            // No response or not parseable, expire in one minute.
+            Log.w(TAG, "License validity timestamp (VT) missing, caching for a minute");
+            lValidityTimestamp = System.currentTimeMillis() + MILLIS_PER_MINUTE;
+            validityTimestamp = Long.toString(lValidityTimestamp);
+        }
+
+        mValidityTimestamp = lValidityTimestamp;
+        mPreferences.putString(PREF_VALIDITY_TIMESTAMP, validityTimestamp);
+    }
+
+    public long getValidityTimestamp() {
+        return mValidityTimestamp;
+    }
+
+    /**
+     * Set the retry until timestamp (GT) received from the server and add to
+     * preferences. You must manually call PreferenceObfuscator.commit() to
+     * commit these changes to disk.
+     * 
+     * @param retryUntil the GT string received
+     */
+    private void setRetryUntil(String retryUntil) {
+        Long lRetryUntil;
+        try {
+            lRetryUntil = Long.parseLong(retryUntil);
+        } catch (NumberFormatException e) {
+            // No response or not parseable, expire immediately
+            Log.w(TAG, "License retry timestamp (GT) missing, grace period disabled");
+            retryUntil = "0";
+            lRetryUntil = 0l;
+        }
+
+        mRetryUntil = lRetryUntil;
+        mPreferences.putString(PREF_RETRY_UNTIL, retryUntil);
+    }
+
+    public long getRetryUntil() {
+        return mRetryUntil;
+    }
+
+    /**
+     * Set the max retries value (GR) as received from the server and add to
+     * preferences. You must manually call PreferenceObfuscator.commit() to
+     * commit these changes to disk.
+     * 
+     * @param maxRetries the GR string received
+     */
+    private void setMaxRetries(String maxRetries) {
+        Long lMaxRetries;
+        try {
+            lMaxRetries = Long.parseLong(maxRetries);
+        } catch (NumberFormatException e) {
+            // No response or not parseable, expire immediately
+            Log.w(TAG, "Licence retry count (GR) missing, grace period disabled");
+            maxRetries = "0";
+            lMaxRetries = 0l;
+        }
+
+        mMaxRetries = lMaxRetries;
+        mPreferences.putString(PREF_MAX_RETRIES, maxRetries);
+    }
+
+    public long getMaxRetries() {
+        return mMaxRetries;
+    }
+
+    /**
+     * Gets the count of expansion URLs. Since expansionURLs are not committed
+     * to preferences, this will return zero if there has been no LVL fetch
+     * in the current session.
+     * 
+     * @return the number of expansion URLs. (0,1,2)
+     */
+    public int getExpansionURLCount() {
+        return mExpansionURLs.size();
+    }
+
+    /**
+     * Gets the expansion URL. Since these URLs are not committed to
+     * preferences, this will always return null if there has not been an LVL
+     * fetch in the current session.
+     * 
+     * @param index the index of the URL to fetch. This value will be either
+     *            MAIN_FILE_URL_INDEX or PATCH_FILE_URL_INDEX
+     * @param URL the URL to set
+     */
+    public String getExpansionURL(int index) {
+        if (index < mExpansionURLs.size()) {
+            return mExpansionURLs.elementAt(index);
+        }
+        return null;
+    }
+
+    /**
+     * Sets the expansion URL. Expansion URL's are not committed to preferences,
+     * but are instead intended to be stored when the license response is
+     * processed by the front-end.
+     * 
+     * @param index the index of the expansion URL. This value will be either
+     *            MAIN_FILE_URL_INDEX or PATCH_FILE_URL_INDEX
+     * @param URL the URL to set
+     */
+    public void setExpansionURL(int index, String URL) {
+        if (index >= mExpansionURLs.size()) {
+            mExpansionURLs.setSize(index + 1);
+        }
+        mExpansionURLs.set(index, URL);
+    }
+
+    public String getExpansionFileName(int index) {
+        if (index < mExpansionFileNames.size()) {
+            return mExpansionFileNames.elementAt(index);
+        }
+        return null;
+    }
+
+    public void setExpansionFileName(int index, String name) {
+        if (index >= mExpansionFileNames.size()) {
+            mExpansionFileNames.setSize(index + 1);
+        }
+        mExpansionFileNames.set(index, name);
+    }
+
+    public long getExpansionFileSize(int index) {
+        if (index < mExpansionFileSizes.size()) {
+            return mExpansionFileSizes.elementAt(index);
+        }
+        return -1;
+    }
+
+    public void setExpansionFileSize(int index, long size) {
+        if (index >= mExpansionFileSizes.size()) {
+            mExpansionFileSizes.setSize(index + 1);
+        }
+        mExpansionFileSizes.set(index, size);
+    }
+
+    /**
+     * {@inheritDoc} This implementation allows access if either:<br>
+     * <ol>
+     * <li>a LICENSED response was received within the validity period
+     * <li>a RETRY response was received in the last minute, and we are under
+     * the RETRY count or in the RETRY period.
+     * </ol>
+     */
+    public boolean allowAccess() {
+        long ts = System.currentTimeMillis();
+        if (mLastResponse == Policy.LICENSED) {
+            // Check if the LICENSED response occurred within the validity
+            // timeout.
+            if (ts <= mValidityTimestamp) {
+                // Cached LICENSED response is still valid.
+                return true;
+            }
+        } else if (mLastResponse == Policy.RETRY &&
+                ts < mLastResponseTime + MILLIS_PER_MINUTE) {
+            // Only allow access if we are within the retry period or we haven't
+            // used up our
+            // max retries.
+            return (ts <= mRetryUntil || mRetryCount <= mMaxRetries);
+        }
+        return false;
+    }
+
+    private Map<String, String> decodeExtras(String extras) {
+        Map<String, String> results = new HashMap<String, String>();
+        try {
+            URI rawExtras = new URI("?" + extras);
+            List<NameValuePair> extraList = URLEncodedUtils.parse(rawExtras, "UTF-8");
+            for (NameValuePair item : extraList) {
+                String name = item.getName();
+                int i = 0;
+                while (results.containsKey(name)) {
+                    name = item.getName() + ++i;
+                }
+                results.put(name, item.getValue());
+            }
+        } catch (URISyntaxException e) {
+            Log.w(TAG, "Invalid syntax error while decoding extras data from server.");
+        }
+        return results;
+    }
+
+}

src/com/google/android/vending/licensing/DeviceLimiter.java

+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.vending.licensing;
+
+/**
+ * Allows the developer to limit the number of devices using a single license.
+ * <p>
+ * The LICENSED response from the server contains a user identifier unique to
+ * the &lt;application, user&gt; pair. The developer can send this identifier
+ * to their own server along with some device identifier (a random number
+ * generated and stored once per application installation,
+ * {@link android.telephony.TelephonyManager#getDeviceId getDeviceId},
+ * {@link android.provider.Settings.Secure#ANDROID_ID ANDROID_ID}, etc).
+ * The more sources used to identify the device, the harder it will be for an
+ * attacker to spoof.
+ * <p>
+ * The server can look at the &lt;application, user, device id&gt; tuple and
+ * restrict a user's application license to run on at most 10 different devices
+ * in a week (for example). We recommend not being too restrictive because a
+ * user might legitimately have multiple devices or be in the process of
+ * changing phones. This will catch egregious violations of multiple people
+ * sharing one license.
+ */
+public interface DeviceLimiter {
+
+    /**
+     * Checks if this device is allowed to use the given user's license.
+     *
+     * @param userId the user whose license the server responded with
+     * @return LICENSED if the device is allowed, NOT_LICENSED if not, RETRY if an error occurs
+     */
+    int isDeviceAllowed(String userId);
+}

src/com/google/android/vending/licensing/ILicenseResultListener.java

+/*
+ * This file is auto-generated.  DO NOT MODIFY.
+ * Original file: aidl/ILicenseResultListener.aidl
+ */
+package com.google.android.vending.licensing;
+import java.lang.String;
+import android.os.RemoteException;
+import android.os.IBinder;
+import android.os.IInterface;
+import android.os.Binder;
+import android.os.Parcel;
+public interface ILicenseResultListener extends android.os.IInterface
+{
+/** Local-side IPC implementation stub class. */
+public static abstract class Stub extends android.os.Binder implements com.google.android.vending.licensing.ILicenseResultListener
+{
+private static final java.lang.String DESCRIPTOR = "com.android.vending.licensing.ILicenseResultListener";
+/** Construct the stub at attach it to the interface. */
+public Stub()
+{
+this.attachInterface(this, DESCRIPTOR);
+}
+/**
+ * Cast an IBinder object into an ILicenseResultListener interface,
+ * generating a proxy if needed.
+ */
+public static com.google.android.vending.licensing.ILicenseResultListener asInterface(android.os.IBinder obj)
+{
+if ((obj==null)) {
+return null;
+}
+android.os.IInterface iin = (android.os.IInterface)obj.queryLocalInterface(DESCRIPTOR);
+if (((iin!=null)&&(iin instanceof com.google.android.vending.licensing.ILicenseResultListener))) {
+return ((com.google.android.vending.licensing.ILicenseResultListener)iin);
+}
+return new com.google.android.vending.licensing.ILicenseResultListener.Stub.Proxy(obj);
+}
+public android.os.IBinder asBinder()
+{
+return this;
+}
+public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException
+{
+switch (code)
+{
+case INTERFACE_TRANSACTION:
+{
+reply.writeString(DESCRIPTOR);
+return true;
+}
+case TRANSACTION_verifyLicense:
+{
+data.enforceInterface(DESCRIPTOR);
+int _arg0;
+_arg0 = data.readInt();
+java.lang.String _arg1;
+_arg1 = data.readString();
+java.lang.String _arg2;
+_arg2 = data.readString();
+this.verifyLicense(_arg0, _arg1, _arg2);
+return true;
+}
+}
+return super.onTransact(code, data, reply, flags);
+}
+private static class Proxy implements com.google.android.vending.licensing.ILicenseResultListener
+{
+private android.os.IBinder mRemote;
+Proxy(android.os.IBinder remote)
+{
+mRemote = remote;
+}
+public android.os.IBinder asBinder()
+{
+return mRemote;
+}
+public java.lang.String getInterfaceDescriptor()
+{
+return DESCRIPTOR;
+}
+public void verifyLicense(int responseCode, java.lang.String signedData, java.lang.String signature) throws android.os.RemoteException
+{
+android.os.Parcel _data = android.os.Parcel.obtain();
+try {
+_data.writeInterfaceToken(DESCRIPTOR);
+_data.writeInt(responseCode);
+_data.writeString(signedData);
+_data.writeString(signature);
+mRemote.transact(Stub.TRANSACTION_verifyLicense, _data, null, IBinder.FLAG_ONEWAY);
+}
+finally {
+_data.recycle();
+}
+}
+}
+static final int TRANSACTION_verifyLicense = (IBinder.FIRST_CALL_TRANSACTION + 0);
+}
+public void verifyLicense(int responseCode, java.lang.String signedData, java.lang.String signature) throws android.os.RemoteException;
+}

src/com/google/android/vending/licensing/ILicensingService.java

+/*
+ * This file is auto-generated.  DO NOT MODIFY.
+ * Original file: aidl/ILicensingService.aidl
+ */
+package com.google.android.vending.licensing;
+import java.lang.String;
+import android.os.RemoteException;
+import android.os.IBinder;
+import android.os.IInterface;
+import android.os.Binder;
+import android.os.Parcel;
+public interface ILicensingService extends android.os.IInterface
+{
+/** Local-side IPC implementation stub class. */
+public static abstract class Stub extends android.os.Binder implements com.google.android.vending.licensing.ILicensingService
+{
+private static final java.lang.String DESCRIPTOR = "com.android.vending.licensing.ILicensingService";
+/** Construct the stub at attach it to the interface. */
+public Stub()
+{
+this.attachInterface(this, DESCRIPTOR);
+}
+/**
+ * Cast an IBinder object into an ILicensingService interface,
+ * generating a proxy if needed.
+ */
+public static com.google.android.vending.licensing.ILicensingService asInterface(android.os.IBinder obj)
+{
+if ((obj==null)) {
+return null;
+}
+android.os.IInterface iin = (android.os.IInterface)obj.queryLocalInterface(DESCRIPTOR);
+if (((iin!=null)&&(iin instanceof com.google.android.vending.licensing.ILicensingService))) {
+return ((com.google.android.vending.licensing.ILicensingService)iin);
+}
+return new com.google.android.vending.licensing.ILicensingService.Stub.Proxy(obj);
+}
+public android.os.IBinder asBinder()
+{
+return this;
+}
+public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException
+{
+switch (code)
+{
+case INTERFACE_TRANSACTION:
+{
+reply.writeString(DESCRIPTOR);
+return true;
+}
+case TRANSACTION_checkLicense:
+{
+data.enforceInterface(DESCRIPTOR);
+long _arg0;
+_arg0 = data.readLong();
+java.lang.String _arg1;
+_arg1 = data.readString();
+com.google.android.vending.licensing.ILicenseResultListener _arg2;
+_arg2 = com.google.android.vending.licensing.ILicenseResultListener.Stub.asInterface(data.readStrongBinder());
+this.checkLicense(_arg0, _arg1, _arg2);
+return true;
+}
+}
+return super.onTransact(code, data, reply, flags);
+}
+private static class Proxy implements com.google.android.vending.licensing.ILicensingService
+{
+private android.os.IBinder mRemote;
+Proxy(android.os.IBinder remote)
+{
+mRemote = remote;
+}
+public android.os.IBinder asBinder()
+{
+return mRemote;
+}
+public java.lang.String getInterfaceDescriptor()
+{
+return DESCRIPTOR;
+}
+public void checkLicense(long nonce, java.lang.String packageName, com.google.android.vending.licensing.ILicenseResultListener listener) throws android.os.RemoteException
+{
+android.os.Parcel _data = android.os.Parcel.obtain();
+try {
+_data.writeInterfaceToken(DESCRIPTOR);
+_data.writeLong(nonce);
+_data.writeString(packageName);
+_data.writeStrongBinder((((listener!=null))?(listener.asBinder()):(null)));
+mRemote.transact(Stub.TRANSACTION_checkLicense, _data, null, IBinder.FLAG_ONEWAY);
+}
+finally {
+_data.recycle();
+}
+}
+}
+static final int TRANSACTION_checkLicense = (IBinder.FIRST_CALL_TRANSACTION + 0);
+}
+public void checkLicense(long nonce, java.lang.String packageName, com.google.android.vending.licensing.ILicenseResultListener listener) throws android.os.RemoteException;
+}

src/com/google/android/vending/licensing/LicenseChecker.java

+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.vending.licensing;
+
+import com.google.android.vending.licensing.util.Base64;
+import com.google.android.vending.licensing.util.Base64DecoderException;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.provider.Settings.Secure;
+import android.util.Log;
+
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.SecureRandom;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.Queue;
+import java.util.Set;
+
+/**
+ * Client library for Android Market license verifications.
+ * <p>
+ * The LicenseChecker is configured via a {@link Policy} which contains the
+ * logic to determine whether a user should have access to the application. For
+ * example, the Policy can define a threshold for allowable number of server or
+ * client failures before the library reports the user as not having access.
+ * <p>
+ * Must also provide the Base64-encoded RSA public key associated with your
+ * developer account. The public key is obtainable from the publisher site.
+ */
+public class LicenseChecker implements ServiceConnection {
+    private static final String TAG = "LicenseChecker";
+
+    private static final String KEY_FACTORY_ALGORITHM = "RSA";
+
+    // Timeout value (in milliseconds) for calls to service.
+    private static final int TIMEOUT_MS = 10 * 1000;
+
+    private static final SecureRandom RANDOM = new SecureRandom();
+    private static final boolean DEBUG_LICENSE_ERROR = false;
+
+    private ILicensingService mService;
+
+    private PublicKey mPublicKey;
+    private final Context mContext;
+    private final Policy mPolicy;
+    /**
+     * A handler for running tasks on a background thread. We don't want license
+     * processing to block the UI thread.
+     */
+    private Handler mHandler;
+    private final String mPackageName;
+    private final String mVersionCode;
+    private final Set<LicenseValidator> mChecksInProgress = new HashSet<LicenseValidator>();
+    private final Queue<LicenseValidator> mPendingChecks = new LinkedList<LicenseValidator>();
+
+    /**
+     * @param context a Context
+     * @param policy implementation of Policy
+     * @param encodedPublicKey Base64-encoded RSA public key
+     * @throws IllegalArgumentException if encodedPublicKey is invalid
+     */
+    public LicenseChecker(Context context, Policy policy, String encodedPublicKey) {
+        mContext = context;
+        mPolicy = policy;
+        mPublicKey = generatePublicKey(encodedPublicKey);
+        mPackageName = mContext.getPackageName();
+        mVersionCode = getVersionCode(context, mPackageName);
+        HandlerThread handlerThread = new HandlerThread("background thread");
+        handlerThread.start();
+        mHandler = new Handler(handlerThread.getLooper());
+    }
+
+    /**
+     * Generates a PublicKey instance from a string containing the
+     * Base64-encoded public key.
+     * 
+     * @param encodedPublicKey Base64-encoded public key
+     * @throws IllegalArgumentException if encodedPublicKey is invalid
+     */
+    private static PublicKey generatePublicKey(String encodedPublicKey) {
+        try {
+            byte[] decodedKey = Base64.decode(encodedPublicKey);
+            KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM);
+
+            return keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey));
+        } catch (NoSuchAlgorithmException e) {
+            // This won't happen in an Android-compatible environment.
+            throw new RuntimeException(e);
+        } catch (Base64DecoderException e) {
+            Log.e(TAG, "Could not decode from Base64.");
+            throw new IllegalArgumentException(e);
+        } catch (InvalidKeySpecException e) {
+            Log.e(TAG, "Invalid key specification.");
+            throw new IllegalArgumentException(e);
+        }
+    }
+
+    /**
+     * Checks if the user should have access to the app.  Binds the service if necessary.
+     * <p>
+     * NOTE: This call uses a trivially obfuscated string (base64-encoded).  For best security,
+     * we recommend obfuscating the string that is passed into bindService using another method
+     * of your own devising.
+     * <p>
+     * source string: "com.android.vending.licensing.ILicensingService"
+     * <p>
+     * @param callback
+     */
+    public synchronized void checkAccess(LicenseCheckerCallback callback) {
+        // If we have a valid recent LICENSED response, we can skip asking
+        // Market.
+        if (mPolicy.allowAccess()) {
+            Log.i(TAG, "Using cached license response");
+            callback.allow(Policy.LICENSED);
+        } else {
+            LicenseValidator validator = new LicenseValidator(mPolicy, new NullDeviceLimiter(),
+                    callback, generateNonce(), mPackageName, mVersionCode);
+
+            if (mService == null) {
+                Log.i(TAG, "Binding to licensing service.");
+                try {
+                    boolean bindResult = mContext
+                            .bindService(
+                                    new Intent(
+                                            new String(
+                                                    Base64.decode("Y29tLmFuZHJvaWQudmVuZGluZy5saWNlbnNpbmcuSUxpY2Vuc2luZ1NlcnZpY2U="))),
+                                    this, // ServiceConnection.
+                                    Context.BIND_AUTO_CREATE);
+
+                    if (bindResult) {
+                        mPendingChecks.offer(validator);
+                    } else {
+                        Log.e(TAG, "Could not bind to service.");
+                        handleServiceConnectionError(validator);
+                    }
+                } catch (SecurityException e) {
+                    callback.applicationError(LicenseCheckerCallback.ERROR_MISSING_PERMISSION);
+                } catch (Base64DecoderException e) {
+                    e.printStackTrace();
+                }
+            } else {
+                mPendingChecks.offer(validator);
+                runChecks();
+            }
+        }
+    }
+
+    private void runChecks() {
+        LicenseValidator validator;
+        while ((validator = mPendingChecks.poll()) != null) {
+            try {
+                Log.i(TAG, "Calling checkLicense on service for " + validator.getPackageName());
+                mService.checkLicense(
+                        validator.getNonce(), validator.getPackageName(),
+                        new ResultListener(validator));
+                mChecksInProgress.add(validator);
+            } catch (RemoteException e) {
+                Log.w(TAG, "RemoteException in checkLicense call.", e);
+                handleServiceConnectionError(validator);
+            }
+        }
+    }
+
+    private synchronized void finishCheck(LicenseValidator validator) {
+        mChecksInProgress.remove(validator);
+        if (mChecksInProgress.isEmpty()) {
+            cleanupService();
+        }
+    }
+
+    private class ResultListener extends ILicenseResultListener.Stub {
+        private final LicenseValidator mValidator;
+        private Runnable mOnTimeout;
+
+        public ResultListener(LicenseValidator validator) {
+            mValidator = validator;
+            mOnTimeout = new Runnable() {
+                public void run() {
+                    Log.i(TAG, "Check timed out.");
+                    handleServiceConnectionError(mValidator);
+                    finishCheck(mValidator);
+                }
+            };
+            startTimeout();
+        }
+
+        private static final int ERROR_CONTACTING_SERVER = 0x101;
+        private static final int ERROR_INVALID_PACKAGE_NAME = 0x102;
+        private static final int ERROR_NON_MATCHING_UID = 0x103;
+
+        // Runs in IPC thread pool. Post it to the Handler, so we can guarantee
+        // either this or the timeout runs.
+        public void verifyLicense(final int responseCode, final String signedData,
+                final String signature) {
+            mHandler.post(new Runnable() {
+                public void run() {
+                    Log.i(TAG, "Received response.");
+                    // Make sure it hasn't already timed out.
+                    if (mChecksInProgress.contains(mValidator)) {
+                        clearTimeout();
+                        mValidator.verify(mPublicKey, responseCode, signedData, signature);
+                        finishCheck(mValidator);
+                    }
+                    if (DEBUG_LICENSE_ERROR) {
+                        boolean logResponse;
+                        String stringError = null;
+                        switch (responseCode) {
+                            case ERROR_CONTACTING_SERVER:
+                                logResponse = true;
+                                stringError = "ERROR_CONTACTING_SERVER";
+                                break;
+                            case ERROR_INVALID_PACKAGE_NAME:
+                                logResponse = true;
+                                stringError = "ERROR_INVALID_PACKAGE_NAME";
+                                break;
+                            case ERROR_NON_MATCHING_UID:
+                                logResponse = true;
+                                stringError = "ERROR_NON_MATCHING_UID";
+                                break;
+                            default:
+                                logResponse = false;
+                        }
+
+                        if (logResponse) {
+                            String android_id = Secure.getString(mContext.getContentResolver(),
+                                    Secure.ANDROID_ID);
+                            Date date = new Date();
+                            Log.d(TAG, "Server Failure: " + stringError);
+                            Log.d(TAG, "Android ID: " + android_id);
+                            Log.d(TAG, "Time: " + date.toGMTString());
+                        }
+                    }
+
+                }
+            });
+        }
+
+        private void startTimeout() {
+            Log.i(TAG, "Start monitoring timeout.");
+            mHandler.postDelayed(mOnTimeout, TIMEOUT_MS);
+        }
+
+        private void clearTimeout() {
+            Log.i(TAG, "Clearing timeout.");
+            mHandler.removeCallbacks(mOnTimeout);
+        }
+    }
+
+    public synchronized void onServiceConnected(ComponentName name, IBinder service) {
+        mService = ILicensingService.Stub.asInterface(service);
+        runChecks();
+    }
+
+    public synchronized void onServiceDisconnected(ComponentName name) {
+        // Called when the connection with the service has been
+        // unexpectedly disconnected. That is, Market crashed.
+        // If there are any checks in progress, the timeouts will handle them.
+        Log.w(TAG, "Service unexpectedly disconnected.");
+        mService = null;
+    }
+
+    /**
+     * Generates policy response for service connection errors, as a result of
+     * disconnections or timeouts.
+     */
+    private synchronized void handleServiceConnectionError(LicenseValidator validator) {
+        mPolicy.processServerResponse(Policy.RETRY, null);
+
+        if (mPolicy.allowAccess()) {
+            validator.getCallback().allow(Policy.RETRY);
+        } else {
+            validator.getCallback().dontAllow(Policy.RETRY);
+        }
+    }
+
+    /** Unbinds service if necessary and removes reference to it. */
+    private void cleanupService() {
+        if (mService != null) {
+            try {
+                mContext.unbindService(this);
+            } catch (IllegalArgumentException e) {
+                // Somehow we've already been unbound. This is a non-fatal
+                // error.
+                Log.e(TAG, "Unable to unbind from licensing service (already unbound)");
+            }
+            mService = null;
+        }
+    }
+
+    /**
+     * Inform the library that the context is about to be destroyed, so that any
+     * open connections can be cleaned up.
+     * <p>
+     * Failure to call this method can result in a crash under certain
+     * circumstances, such as during screen rotation if an Activity requests the
+     * license check or when the user exits the application.
+     */
+    public synchronized void onDestroy() {
+        cleanupService();
+        mHandler.getLooper().quit();
+    }
+
+    /** Generates a nonce (number used once). */
+    private int generateNonce() {
+        return RANDOM.nextInt();
+    }
+
+    /**
+     * Get version code for the application package name.
+     * 
+     * @param context
+     * @param packageName application package name
+     * @return the version code or empty string if package not found
+     */
+    private static String getVersionCode(Context context, String packageName) {
+        try {
+            return String.valueOf(context.getPackageManager().getPackageInfo(packageName, 0).
+                    versionCode);
+        } catch (NameNotFoundException e) {
+            Log.e(TAG, "Package not found. could not get version code.");
+            return "";
+        }
+    }
+}

src/com/google/android/vending/licensing/LicenseCheckerCallback.java

+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.vending.licensing;
+
+/**
+ * Callback for the license checker library.
+ * <p>
+ * Upon checking with the Market server and conferring with the {@link Policy},
+ * the library calls the appropriate callback method to communicate the result.
+ * <p>
+ * <b>The callback does not occur in the original checking thread.</b> Your
+ * application should post to the appropriate handling thread or lock
+ * accordingly.
+ * <p>
+ * The reason that is passed back with allow/dontAllow is the base status handed
+ * to the policy for allowed/disallowing the license. Policy.RETRY will call
+ * allow or dontAllow depending on other statistics associated with the policy,
+ * while in most cases Policy.NOT_LICENSED will call dontAllow and
+ * Policy.LICENSED will Allow.
+ */
+public interface LicenseCheckerCallback {
+
+    /**
+     * Allow use. App should proceed as normal.
+     * 
+     * @param reason Policy.LICENSED or Policy.RETRY typically. (although in
+     *            theory the policy can return Policy.NOT_LICENSED here as well)
+     */
+    public void allow(int reason);
+
+    /**
+     * Don't allow use. App should inform user and take appropriate action.
+     * 
+     * @param reason Policy.NOT_LICENSED or Policy.RETRY. (although in theory
+     *            the policy can return Policy.LICENSED here as well ---
+     *            perhaps the call to the LVL took too long, for example)
+     */
+    public void dontAllow(int reason);
+
+    /** Application error codes. */
+    public static final int ERROR_INVALID_PACKAGE_NAME = 1;
+    public static final int ERROR_NON_MATCHING_UID = 2;
+    public static final int ERROR_NOT_MARKET_MANAGED = 3;
+    public static final int ERROR_CHECK_IN_PROGRESS = 4;
+    public static final int ERROR_INVALID_PUBLIC_KEY = 5;
+    public static final int ERROR_MISSING_PERMISSION = 6;
+
+    /**
+     * Error in application code. Caller did not call or set up license checker
+     * correctly. Should be considered fatal.
+     */
+    public void applicationError(int errorCode);
+}

src/com/google/android/vending/licensing/LicenseValidator.java

+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.vending.licensing;
+
+import com.google.android.vending.licensing.util.Base64;
+import com.google.android.vending.licensing.util.Base64DecoderException;
+
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.security.SignatureException;
+
+/**
+ * Contains data related to a licensing request and methods to verify
+ * and process the response.
+ */
+class LicenseValidator {
+    private static final String TAG = "LicenseValidator";
+
+    // Server response codes.
+    private static final int LICENSED = 0x0;
+    private static final int NOT_LICENSED = 0x1;
+    private static final int LICENSED_OLD_KEY = 0x2;
+    private static final int ERROR_NOT_MARKET_MANAGED = 0x3;
+    private static final int ERROR_SERVER_FAILURE = 0x4;
+    private static final int ERROR_OVER_QUOTA = 0x5;
+
+    private static final int ERROR_CONTACTING_SERVER = 0x101;
+    private static final int ERROR_INVALID_PACKAGE_NAME = 0x102;
+    private static final int ERROR_NON_MATCHING_UID = 0x103;
+
+    private final Policy mPolicy;
+    private final LicenseCheckerCallback mCallback;
+    private final int mNonce;
+    private final String mPackageName;
+    private final String mVersionCode;
+    private final DeviceLimiter mDeviceLimiter;
+
+    LicenseValidator(Policy policy, DeviceLimiter deviceLimiter, LicenseCheckerCallback callback,
+             int nonce, String packageName, String versionCode) {
+        mPolicy = policy;
+        mDeviceLimiter = deviceLimiter;
+        mCallback = callback;
+        mNonce = nonce;
+        mPackageName = packageName;
+        mVersionCode = versionCode;
+    }
+
+    public LicenseCheckerCallback getCallback() {
+        return mCallback;
+    }
+
+    public int getNonce() {
+        return mNonce;
+    }
+
+    public String getPackageName() {
+        return mPackageName;
+    }
+
+    private static final String SIGNATURE_ALGORITHM = "SHA1withRSA";
+
+    /**
+     * Verifies the response from server and calls appropriate callback method.
+     *
+     * @param publicKey public key associated with the developer account
+     * @param responseCode server response code
+     * @param signedData signed data from server
+     * @param signature server signature
+     */
+    public void verify(PublicKey publicKey, int responseCode, String signedData, String signature) {
+        String userId = null;
+        // Skip signature check for unsuccessful requests
+        ResponseData data = null;
+        if (responseCode == LICENSED || responseCode == NOT_LICENSED ||
+                responseCode == LICENSED_OLD_KEY) {
+            // Verify signature.
+            try {
+                Signature sig = Signature.getInstance(SIGNATURE_ALGORITHM);
+                sig.initVerify(publicKey);
+                sig.update(signedData.getBytes());
+
+                if (!sig.verify(Base64.decode(signature))) {
+                    Log.e(TAG, "Signature verification failed.");
+                    handleInvalidResponse();
+                    return;
+                }
+            } catch (NoSuchAlgorithmException e) {
+                // This can't happen on an Android compatible device.
+                throw new RuntimeException(e);
+            } catch (InvalidKeyException e) {
+                handleApplicationError(LicenseCheckerCallback.ERROR_INVALID_PUBLIC_KEY);
+                return;
+            } catch (SignatureException e) {
+                throw new RuntimeException(e);
+            } catch (Base64DecoderException e) {
+                Log.e(TAG, "Could not Base64-decode signature.");
+                handleInvalidResponse();
+                return;
+            }
+
+            // Parse and validate response.
+            try {
+                data = ResponseData.parse(signedData);
+            } catch (IllegalArgumentException e) {
+                Log.e(TAG, "Could not parse response.");
+                handleInvalidResponse();
+                return;
+            }
+
+            if (data.responseCode != responseCode) {
+                Log.e(TAG, "Response codes don't match.");
+                handleInvalidResponse();
+                return;
+            }
+
+            if (data.nonce != mNonce) {
+                Log.e(TAG, "Nonce doesn't match.");
+                handleInvalidResponse();
+                return;
+            }
+
+            if (!data.packageName.equals(mPackageName)) {
+                Log.e(TAG, "Package name doesn't match.");
+                handleInvalidResponse();
+                return;
+            }
+
+            if (!data.versionCode.equals(mVersionCode)) {
+                Log.e(TAG, "Version codes don't match.");
+                handleInvalidResponse();
+                return;
+            }
+
+            // Application-specific user identifier.
+            userId = data.userId;
+            if (TextUtils.isEmpty(userId)) {
+                Log.e(TAG, "User identifier is empty.");
+                handleInvalidResponse();
+                return;
+            }
+        }
+
+        switch (responseCode) {
+            case LICENSED:
+            case LICENSED_OLD_KEY:
+                int limiterResponse = mDeviceLimiter.isDeviceAllowed(userId);
+                handleResponse(limiterResponse, data);
+                break;
+            case NOT_LICENSED:
+                handleResponse(Policy.NOT_LICENSED, data);
+                break;
+            case ERROR_CONTACTING_SERVER:
+                Log.w(TAG, "Error contacting licensing server.");
+                handleResponse(Policy.RETRY, data);
+                break;
+            case ERROR_SERVER_FAILURE:
+                Log.w(TAG, "An error has occurred on the licensing server.");
+                handleResponse(Policy.RETRY, data);
+                break;
+            case ERROR_OVER_QUOTA:
+                Log.w(TAG, "Licensing server is refusing to talk to this device, over quota.");
+                handleResponse(Policy.RETRY, data);
+                break;
+            case ERROR_INVALID_PACKAGE_NAME:
+                handleApplicationError(LicenseCheckerCallback.ERROR_INVALID_PACKAGE_NAME);
+                break;
+            case ERROR_NON_MATCHING_UID:
+                handleApplicationError(LicenseCheckerCallback.ERROR_NON_MATCHING_UID);
+                break;
+            case ERROR_NOT_MARKET_MANAGED:
+                handleApplicationError(LicenseCheckerCallback.ERROR_NOT_MARKET_MANAGED);
+                break;
+            default:
+                Log.e(TAG, "Unknown response code for license check.");
+                handleInvalidResponse();
+        }
+    }
+
+    /**
+     * Confers with policy and calls appropriate callback method.
+     *
+     * @param response
+     * @param rawData
+     */
+    private void handleResponse(int response, ResponseData rawData) {
+        // Update policy data and increment retry counter (if needed)
+        mPolicy.processServerResponse(response, rawData);
+
+        // Given everything we know, including cached data, ask the policy if we should grant
+        // access.
+        if (mPolicy.allowAccess()) {
+            mCallback.allow(response);
+        } else {
+            mCallback.dontAllow(response);
+        }
+    }
+
+    private void handleApplicationError(int code) {
+        mCallback.applicationError(code);
+    }
+
+    private void handleInvalidResponse() {
+        mCallback.dontAllow(Policy.NOT_LICENSED);
+    }
+}

src/com/google/android/vending/licensing/NullDeviceLimiter.java

+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.vending.licensing;
+
+/**
+ * A DeviceLimiter that doesn't limit the number of devices that can use a
+ * given user's license.
+ * <p>
+ * Unless you have reason to believe that your application is being pirated
+ * by multiple users using the same license (signing in to Market as the same
+ * user), we recommend you use this implementation.
+ */
+public class NullDeviceLimiter implements DeviceLimiter {
+
+    public int isDeviceAllowed(String userId) {
+        return Policy.LICENSED;
+    }
+}

src/com/google/android/vending/licensing/Obfuscator.java

+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.vending.licensing;
+
+/**
+ * Interface used as part of a {@link Policy} to allow application authors to obfuscate
+ * licensing data that will be stored into a SharedPreferences file.
+ * <p>
+ * Any transformation scheme must be reversable. Implementing classes may optionally implement an
+ * integrity check to further prevent modification to preference data. Implementing classes
+ * should use device-specific information as a key in the obfuscation algorithm to prevent
+ * obfuscated preferences from being shared among devices.
+ */
+public interface Obfuscator {
+
+    /**
+     * Obfuscate a string that is being stored into shared preferences.
+     *
+     * @param original The data that is to be obfuscated.
+     * @param key The key for the data that is to be obfuscated.
+     * @return A transformed version of the original data.
+     */
+    String obfuscate(String original, String key);
+
+    /**
+     * Undo the transformation applied to data by the obfuscate() method.
+     *
+     * @param original The data that is to be obfuscated.
+     * @param key The key for the data that is to be obfuscated.
+     * @return A transformed version of the original data.
+     * @throws ValidationException Optionally thrown if a data integrity check fails.
+     */
+    String unobfuscate(String obfuscated, String key) throws ValidationException;
+}

src/com/google/android/vending/licensing/Policy.java

+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.vending.licensing;
+
+/**
+ * Policy used by {@link LicenseChecker} to determine whether a user should have
+ * access to the application.
+ */
+public interface Policy {
+
+    /**
+     * Change these values to make it more difficult for tools to automatically
+     * strip LVL protection from your APK.
+     */
+
+    /**
+     * LICENSED means that the server returned back a valid license response