diff --git a/samples/MarketLicensing/src/com/android/vending/licensing/ILicenseResultListener.aidl b/samples/MarketLicensing/src/com/android/vending/licensing/ILicenseResultListener.aidl new file mode 100755 index 000000000..869cb16f6 --- /dev/null +++ b/samples/MarketLicensing/src/com/android/vending/licensing/ILicenseResultListener.aidl @@ -0,0 +1,21 @@ +/* + * 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; + +oneway interface ILicenseResultListener { + void verifyLicense(int responseCode, String signedData, String signature); +} diff --git a/samples/MarketLicensing/src/com/android/vending/licensing/ILicensingService.aidl b/samples/MarketLicensing/src/com/android/vending/licensing/ILicensingService.aidl new file mode 100755 index 000000000..9541a2090 --- /dev/null +++ b/samples/MarketLicensing/src/com/android/vending/licensing/ILicensingService.aidl @@ -0,0 +1,23 @@ +/* + * 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; + +oneway interface ILicensingService { + void checkLicense(long nonce, String packageName, in ILicenseResultListener listener); +} diff --git a/samples/MarketLicensing/src/com/android/vending/licensing/LicenseChecker.java b/samples/MarketLicensing/src/com/android/vending/licensing/LicenseChecker.java new file mode 100755 index 000000000..773b8cdeb --- /dev/null +++ b/samples/MarketLicensing/src/com/android/vending/licensing/LicenseChecker.java @@ -0,0 +1,165 @@ +/* + * 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 java.security.SecureRandom; + +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.IBinder; +import android.os.RemoteException; +import android.util.Log; + +import com.android.vending.licensing.LicenseCheckerCallback.ApplicationErrorCode; +import com.android.vending.licensing.Policy.LicenseResponse; + +/** + * Client library for Android Market license verifications. + * + * 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. + * + * This library is not thread-safe. Multiple, concurrent checks will result in + * an error. + */ +public class LicenseChecker implements ServiceConnection { + private static final String TAG = "LicenseChecker"; + + private static final SecureRandom RANDOM = new SecureRandom(); + + private ILicensingService mService; + + /** Validator for the request in progress. */ + private LicenseValidator mValidator; + + private final Context mContext; + private final Policy mPolicy; + /** Listener for service (IPC) calls. */ + private final ResultListener mListener; + private final String mPackageName; + private final String mVersionCode; + + public LicenseChecker(Context context, Policy policy) { + mContext = context; + mPolicy = policy; + mListener = new ResultListener(); + mPackageName = mContext.getPackageName(); + mVersionCode = getVersionCode(context, mPackageName); + } + + private boolean isInProgress() { + return mValidator != null; + } + + /** + * Checks if the user should have access to the app. + * + * @param callback + */ + public synchronized void checkAccess(LicenseCheckerCallback callback) { + if (isInProgress()) { + callback.applicationError(ApplicationErrorCode.CHECK_IN_PROGRESS); + } + + mValidator = new LicenseValidator(mPolicy, callback, generateNonce(), mPackageName, + mVersionCode); + + Log.i(TAG, "Binding to licensing service."); + boolean bindResult = mContext.bindService(new Intent(ILicensingService.class.getName()), + this, // ServiceConnection. + Context.BIND_AUTO_CREATE); + + if (!bindResult) { + Log.e(TAG, "Could not bind to service."); + callback.dontAllow(); + // No need to unbind at this point. + return; + } + } + + private class ResultListener extends ILicenseResultListener.Stub { + public void verifyLicense(int responseCode, String signedData, String signature) { + mValidator.verify(responseCode, signedData, signature); + cleanup(); + } + } + + public void onServiceConnected(ComponentName name, IBinder service) { + mService = ILicensingService.Stub.asInterface(service); + + try { + Log.i(TAG, "Calling checkLicense on service for " + mValidator.getPackageName()); + mService.checkLicense(mValidator.getNonce(), mValidator.getPackageName(), mListener); + } catch (RemoteException e) { + Log.w(TAG, "RemoteException in checkLicense call.", e); + handleServiceConnectionError(); + // cleanup unbinds service. + cleanup(); + } + } + + public void onServiceDisconnected(ComponentName name) { + // Called when the connection with the service has been + // unexpectedly disconnected. That is, Market crashed. + Log.w(TAG, "Service unexpectedly disconnected."); + handleServiceConnectionError(); + // cleanup unbinds service. + cleanup(); + } + + private void handleServiceConnectionError() { + if (mPolicy.allowAccess(LicenseResponse.CLIENT_RETRY)) { + mValidator.getCallback().allow(); + } else { + mValidator.getCallback().dontAllow(); + } + } + + /** Resets request state. */ + private synchronized void cleanup() { + mContext.unbindService(this); + mValidator = null; + } + + /** 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 ""; + } + } +} diff --git a/samples/MarketLicensing/src/com/android/vending/licensing/LicenseCheckerCallback.java b/samples/MarketLicensing/src/com/android/vending/licensing/LicenseCheckerCallback.java new file mode 100755 index 000000000..156749724 --- /dev/null +++ b/samples/MarketLicensing/src/com/android/vending/licensing/LicenseCheckerCallback.java @@ -0,0 +1,55 @@ +/* + * 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; + +/** + * Callback for the license checker library. + * + * Upon checking with the Market server and conferring with the policy, the + * library calls a appropriate callback method to communicate the result. + */ +public interface LicenseCheckerCallback { + + /** + * Allow use. App should proceed as normal. + */ + public void allow(); + + /** + * Don't allow use. App should inform user and take appropriate action. + */ + public void dontAllow(); + + /** Application error codes. */ + public enum ApplicationErrorCode { + /** Package is not installed. */ + INVALID_PACKAGE_NAME, + /** Requested for a package that is not the current app. */ + NON_MATCHING_UID, + /** Market does not know about the package. */ + NOT_MARKET_MANAGED, + /** A previous check request is already in progress. + * Only one check is allowed at a time. */ + CHECK_IN_PROGRESS + } + + /** + * Error in application code. Caller did not call or set up license + * checker correctly. Should be considered fatal. + */ + public void applicationError(ApplicationErrorCode errorCode); +} diff --git a/samples/MarketLicensing/src/com/android/vending/licensing/LicenseValidator.java b/samples/MarketLicensing/src/com/android/vending/licensing/LicenseValidator.java new file mode 100755 index 000000000..135d98e99 --- /dev/null +++ b/samples/MarketLicensing/src/com/android/vending/licensing/LicenseValidator.java @@ -0,0 +1,165 @@ +/* + * 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 android.util.Log; + +import com.android.vending.licensing.LicenseCheckerCallback.ApplicationErrorCode; +import com.android.vending.licensing.Policy.LicenseResponse; + +/** + * 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_INVALID_KEYS = 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; + + LicenseValidator(Policy policy, LicenseCheckerCallback callback, int nonce, String packageName, + String versionCode) { + mPolicy = policy; + mCallback = callback; + mNonce = nonce; + mPackageName = packageName; + mVersionCode = versionCode; + } + + public LicenseCheckerCallback getCallback() { + return mCallback; + } + + public int getNonce() { + return mNonce; + } + + public String getPackageName() { + return mPackageName; + } + + /** + * Verifies the response from server and calls appropriate callback method. + * + * @param responseCode server response code + * @param signedData signed data from server + * @param signature server signature + */ + public void verify(int responseCode, String signedData, String signature) { + // Parse and validate response. + // TODO(jyum): decode data with signature. + // TODO(jyum): verify timestamp is within reason. However, relying + // on device clock may lead to problems? + ResponseData data; + 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; + } + + switch (responseCode) { + case LICENSED: + case LICENSED_OLD_KEY: + handleResponse(LicenseResponse.LICENSED); + break; + case NOT_LICENSED: + handleResponse(LicenseResponse.NOT_LICENSED); + break; + case ERROR_CONTACTING_SERVER: + handleResponse(LicenseResponse.CLIENT_RETRY); + break; + case ERROR_INVALID_KEYS: + case ERROR_OVER_QUOTA: + handleResponse(LicenseResponse.SERVER_RETRY); + break; + case ERROR_INVALID_PACKAGE_NAME: + handleApplicationError(ApplicationErrorCode.INVALID_PACKAGE_NAME); + break; + case ERROR_NON_MATCHING_UID: + handleApplicationError(ApplicationErrorCode.NON_MATCHING_UID); + break; + case ERROR_NOT_MARKET_MANAGED: + handleApplicationError(ApplicationErrorCode.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 + */ + private void handleResponse(LicenseResponse response) { + if (mPolicy.allowAccess(response)) { + mCallback.allow(); + } else { + mCallback.dontAllow(); + } + } + + private void handleApplicationError(ApplicationErrorCode code) { + mCallback.applicationError(code); + } + + private void handleInvalidResponse() { + mCallback.dontAllow(); + } +} diff --git a/samples/MarketLicensing/src/com/android/vending/licensing/Policy.java b/samples/MarketLicensing/src/com/android/vending/licensing/Policy.java new file mode 100755 index 000000000..461c08e88 --- /dev/null +++ b/samples/MarketLicensing/src/com/android/vending/licensing/Policy.java @@ -0,0 +1,55 @@ +/* + * 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; + +/** + * Policy used by {@link LicenseChecker} to determine whether a user should + * have access to the application. + */ +public interface Policy { + + /** + * Result of a license check. + */ + public enum LicenseResponse { + /** + * User is licensed to use the app. + */ + LICENSED, + /** + * User is not licensed to use the app. + */ + NOT_LICENSED, + /** + * Retryable error on the client side e.g. no network. + */ + CLIENT_RETRY, + /** + * Retryable error on the server side e.g. application is over request + * quota. + */ + SERVER_RETRY, + } + + /** + * Determines whether the user should be allowed access. + * + * @param response result of the license check request + * @return true iff access should be allowed + */ + boolean allowAccess(LicenseResponse response); +} diff --git a/samples/MarketLicensing/src/com/android/vending/licensing/ResponseData.java b/samples/MarketLicensing/src/com/android/vending/licensing/ResponseData.java new file mode 100755 index 000000000..3882d5615 --- /dev/null +++ b/samples/MarketLicensing/src/com/android/vending/licensing/ResponseData.java @@ -0,0 +1,84 @@ +/* + * 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 java.util.Iterator; +import java.util.regex.Pattern; + +import android.text.TextUtils; + +/** + * ResponseData from licensing server. + */ +class ResponseData { + + public int responseCode; + public int nonce; + public String packageName; + public String versionCode; + public String userId; + public long timestamp; + /** Response-specific data. */ + public String extra; + + /** + * Parses response string into ResponseData. + * + * @param responseData response data string + * @throws IllegalArgumentException upon parsing error + * @return ResponseData object + */ + public static ResponseData parse(String responseData) { + // Must parse out main response data and response-specific data. + TextUtils.StringSplitter splitter = new TextUtils.SimpleStringSplitter(':'); + splitter.setString(responseData); + Iterator it = splitter.iterator(); + if (!it.hasNext()) { + throw new IllegalArgumentException("Blank response."); + } + final String mainData = it.next(); + + // Response-specific (extra) data is optional. + String extraData = ""; + if (it.hasNext()) { + extraData = it.next(); + } + + String [] fields = TextUtils.split(mainData, Pattern.quote("|")); + if (fields.length < 5) { + throw new IllegalArgumentException("Wrong number of fields."); + } + + ResponseData data = new ResponseData(); + data.extra = extraData; + data.responseCode = Integer.parseInt(fields[0]); + data.nonce = Integer.parseInt(fields[1]); + data.packageName = fields[2]; + data.versionCode = fields[3]; + // TODO(jyum): userId is not there yet. + // data.userId = fields[4]; + data.timestamp = Long.parseLong(fields[4]); + + return data; + } + + @Override + public String toString() { + return TextUtils.join("|", new Object [] { responseCode, nonce, packageName, versionCode, + userId, timestamp }); + } +} diff --git a/samples/MarketLicensing/src/com/android/vending/licensing/StrictPolicy.java b/samples/MarketLicensing/src/com/android/vending/licensing/StrictPolicy.java new file mode 100755 index 000000000..ddff8e973 --- /dev/null +++ b/samples/MarketLicensing/src/com/android/vending/licensing/StrictPolicy.java @@ -0,0 +1,30 @@ +/* + * 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; + +/** + * Strict policy. + * + * Should never be used in a real application as it strictly disallows access + * upon retryable errors such as no connection present. + */ +public class StrictPolicy implements Policy { + + public boolean allowAccess(LicenseResponse response) { + return LicenseResponse.LICENSED == response; + } +}