/*
 * 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 android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.PackageManager.NameNotFoundException;
import android.net.Uri;
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 com.google.android.vending.licensing.ILicenseResultListener;
import com.google.android.vending.licensing.ILicensingService;
import com.google.android.vending.licensing.util.Base64;
import com.google.android.vending.licensing.util.Base64DecoderException;

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 Google Play 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 encoded -
																		 // com.android.vending.licensing.ILicensingService
																		 // Consider encoding this in another way in your
																		 // code to improve security
																		 Base64.decode(
																				 "Y29tLmFuZHJvaWQudmVuZGluZy5saWNlbnNpbmcuSUxpY2Vuc2luZ1NlcnZpY2U=")))
																 // As of Android 5.0, implicit
																 // Service Intents are no longer
																 // allowed because it's not
																 // possible for the user to
																 // participate in disambiguating
																 // them. This does mean we break
																 // compatibility with Android
																 // Cupcake devices with this
																 // release, since setPackage was
																 // added in Donut.
																 .setPackage(
																		 new String(
																				 // Base64
																				 // encoded -
																				 // com.android.vending
																				 Base64.decode(
																						 "Y29tLmFuZHJvaWQudmVuZGluZw=="))),
														 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();
			}
		}
	}

	/**
     * Triggers the last deep link licensing URL returned from the server, which redirects users to a
     * page which enables them to gain access to the app. If no such URL is returned by the server, it
     * will go to the details page of the app in the Play Store.
     */
	public void followLastLicensingUrl(Context context) {
		String licensingUrl = mPolicy.getLicensingUrl();
		if (licensingUrl == null) {
			licensingUrl = "https://play.google.com/store/apps/details?id=" + context.getPackageName();
		}
		Intent marketIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(licensingUrl));
		context.startActivity(marketIntent);
	}

	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.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 "";
		}
	}
}
