
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 android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;

import com.google.android.vending.licensing.util.URIQueryDecoder;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
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,
 * error retry count and a URL for restoring app access in unlicensed cases.
 * <p>
 * These values will vary based on the the way the application is configured in
 * the Google Play 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.google.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 PREF_LICENSING_URL = "licensingUrl";
	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 String mLicensingUrl;
	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));
		mLicensingUrl = mPreferences.getString(PREF_LICENSING_URL, null);
	}

	/**
     * 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
     * <li>LU: a deep link URL that can enable access for unlicensed apps (e.g.
     * buy app on the Play Store)
     * </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);
		}

		// Update server policy data
		Map<String, String> extras = decodeExtras(rawData);
		if (response == Policy.LICENSED) {
			mLastResponse = response;
			// Reset the licensing URL since it is only applicable for NOT_LICENSED responses.
			setLicensingUrl(null);
			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 retry params
			setValidityTimestamp(DEFAULT_VALIDITY_TIMESTAMP);
			setRetryUntil(DEFAULT_RETRY_UNTIL);
			setMaxRetries(DEFAULT_MAX_RETRIES);
			// Update the licensing URL
			setLicensingUrl(extras.get("LU"));
		}

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

	/**
     * Set the licensing URL that displays a Play Store UI for the user to regain app access.
     *
     * @param url the LU string received
     */
	private void setLicensingUrl(String url) {
		mLicensingUrl = url;
		mPreferences.putString(PREF_LICENSING_URL, url);
	}

	public String getLicensingUrl() {
		return mLicensingUrl;
	}

	/**
     * 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
     */
	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(
			com.google.android.vending.licensing.ResponseData rawData) {
		Map<String, String> results = new HashMap<String, String>();
		if (rawData == null) {
			return results;
		}

		try {
			URI rawExtras = new URI("?" + rawData.extra);
			URIQueryDecoder.DecodeQuery(rawExtras, results);
		} catch (URISyntaxException e) {
			Log.w(TAG, "Invalid syntax error while decoding extras data from server.");
		}
		return results;
	}
}
