package moe.yuuta.gplicense; import android.content.Context; import android.content.SharedPreferences; import com.elvishew.xlog.Logger; import com.elvishew.xlog.XLog; import moe.yuuta.gplicense.util.URIQueryDecoder; import java.net.URI; import java.net.URISyntaxException; import java.util.HashMap; import java.util.Map; /** * 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. *

* 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. *

* Developers who need more fine grained control over their application's * licensing policy should implement a custom Policy. */ public class ServerManagedPolicy implements Policy { private final Logger logger = XLog.tag("DefPolicy").build(); private static final String PREFS_FILE = "DefPol"; private static final String PREF_LAST_RESPONSE = "la_resp"; private static final String PREF_VALIDITY_TIMESTAMP = "ts"; private static final String PREF_RETRY_UNTIL = "ret_utl"; private static final String PREF_MAX_RETRIES = "max_ret"; private static final String PREF_RETRY_COUNT = "ret_am"; private static final String PREF_LICENSING_URL = "l_url"; 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; /** * @param context The hostContext for the current application * @param obfuscator An obfuscator to be used with preferences. */ public ServerManagedPolicy(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); } /** * Process a new response from the license server. *

* This data will be used for computing future policy decisions. The * following parameters are processed: *

* * @param response the result from validating the server response * @param rawData the raw server response data */ public void processServerResponse(int response, ResponseData rawData) { // Update retry counter if (response != Policy.RETRY) { setRetryCount(0); } else { setRetryCount(mRetryCount + 1); } // Update server policy data Map 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(extras.get("VT")); setRetryUntil(extras.get("GT")); setMaxRetries(extras.get("GR")); } 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 parsable, expire in one minute. logger.w("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 parsable, expire immediately logger.w("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 parsable, expire immediately logger.w("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 license URL value (LU) as received from the server and add to preferences. You must * manually call PreferenceObfuscator.commit() to commit these changes to disk. * * @param url the LU string received */ private void setLicensingUrl(String url) { mLicensingUrl = url; mPreferences.putString(PREF_LICENSING_URL, url); } public String getLicensingUrl() { return mLicensingUrl; } /** * {@inheritDoc} * * This implementation allows access if either:
*
    *
  1. a LICENSED response was received within the validity period *
  2. a RETRY response was received in the last minute, and we are under * the RETRY count or in the RETRY period. *
*/ 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 decodeExtras(ResponseData rawData) { Map results = new HashMap(); if (rawData == null) { return results; } try { URI rawExtras = new URI("?" + rawData.extra); URIQueryDecoder.DecodeQuery(rawExtras, results); } catch (URISyntaxException e) { logger.w("Invalid syntax error while decoding extras data from server."); } return results; } }