1 2 package com.google.android.vending.licensing; 3 4 /* 5 * Copyright (C) 2012 The Android Open Source Project 6 * 7 * Licensed under the Apache License, Version 2.0 (the "License"); 8 * you may not use this file except in compliance with the License. 9 * You may obtain a copy of the License at 10 * 11 * http://www.apache.org/licenses/LICENSE-2.0 12 * 13 * Unless required by applicable law or agreed to in writing, software 14 * distributed under the License is distributed on an "AS IS" BASIS, 15 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 * See the License for the specific language governing permissions and 17 * limitations under the License. 18 */ 19 20 import org.apache.http.NameValuePair; 21 import org.apache.http.client.utils.URLEncodedUtils; 22 23 import android.content.Context; 24 import android.content.SharedPreferences; 25 import android.util.Log; 26 27 import java.net.URI; 28 import java.net.URISyntaxException; 29 import java.util.HashMap; 30 import java.util.List; 31 import java.util.Map; 32 import java.util.Set; 33 import java.util.Vector; 34 35 /** 36 * Default policy. All policy decisions are based off of response data received 37 * from the licensing service. Specifically, the licensing server sends the 38 * following information: response validity period, error retry period, and 39 * error retry count. 40 * <p> 41 * These values will vary based on the the way the application is configured in 42 * the Android Market publishing console, such as whether the application is 43 * marked as free or is within its refund period, as well as how often an 44 * application is checking with the licensing service. 45 * <p> 46 * Developers who need more fine grained control over their application's 47 * licensing policy should implement a custom Policy. 48 */ 49 public class APKExpansionPolicy implements Policy { 50 51 private static final String TAG = "APKExpansionPolicy"; 52 private static final String PREFS_FILE = "com.android.vending.licensing.APKExpansionPolicy"; 53 private static final String PREF_LAST_RESPONSE = "lastResponse"; 54 private static final String PREF_VALIDITY_TIMESTAMP = "validityTimestamp"; 55 private static final String PREF_RETRY_UNTIL = "retryUntil"; 56 private static final String PREF_MAX_RETRIES = "maxRetries"; 57 private static final String PREF_RETRY_COUNT = "retryCount"; 58 private static final String DEFAULT_VALIDITY_TIMESTAMP = "0"; 59 private static final String DEFAULT_RETRY_UNTIL = "0"; 60 private static final String DEFAULT_MAX_RETRIES = "0"; 61 private static final String DEFAULT_RETRY_COUNT = "0"; 62 63 private static final long MILLIS_PER_MINUTE = 60 * 1000; 64 65 private long mValidityTimestamp; 66 private long mRetryUntil; 67 private long mMaxRetries; 68 private long mRetryCount; 69 private long mLastResponseTime = 0; 70 private int mLastResponse; 71 private PreferenceObfuscator mPreferences; 72 private Vector<String> mExpansionURLs = new Vector<String>(); 73 private Vector<String> mExpansionFileNames = new Vector<String>(); 74 private Vector<Long> mExpansionFileSizes = new Vector<Long>(); 75 76 /** 77 * The design of the protocol supports n files. Currently the market can 78 * only deliver two files. To accommodate this, we have these two constants, 79 * but the order is the only relevant thing here. 80 */ 81 public static final int MAIN_FILE_URL_INDEX = 0; 82 public static final int PATCH_FILE_URL_INDEX = 1; 83 84 /** 85 * @param context The context for the current application 86 * @param obfuscator An obfuscator to be used with preferences. 87 */ APKExpansionPolicy(Context context, Obfuscator obfuscator)88 public APKExpansionPolicy(Context context, Obfuscator obfuscator) { 89 // Import old values 90 SharedPreferences sp = context.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE); 91 mPreferences = new PreferenceObfuscator(sp, obfuscator); 92 mLastResponse = Integer.parseInt( 93 mPreferences.getString(PREF_LAST_RESPONSE, Integer.toString(Policy.RETRY))); 94 mValidityTimestamp = Long.parseLong(mPreferences.getString(PREF_VALIDITY_TIMESTAMP, 95 DEFAULT_VALIDITY_TIMESTAMP)); 96 mRetryUntil = Long.parseLong(mPreferences.getString(PREF_RETRY_UNTIL, DEFAULT_RETRY_UNTIL)); 97 mMaxRetries = Long.parseLong(mPreferences.getString(PREF_MAX_RETRIES, DEFAULT_MAX_RETRIES)); 98 mRetryCount = Long.parseLong(mPreferences.getString(PREF_RETRY_COUNT, DEFAULT_RETRY_COUNT)); 99 } 100 101 /** 102 * We call this to guarantee that we fetch a fresh policy from the server. 103 * This is to be used if the URL is invalid. 104 */ resetPolicy()105 public void resetPolicy() { 106 mPreferences.putString(PREF_LAST_RESPONSE, Integer.toString(Policy.RETRY)); 107 setRetryUntil(DEFAULT_RETRY_UNTIL); 108 setMaxRetries(DEFAULT_MAX_RETRIES); 109 setRetryCount(Long.parseLong(DEFAULT_RETRY_COUNT)); 110 setValidityTimestamp(DEFAULT_VALIDITY_TIMESTAMP); 111 mPreferences.commit(); 112 } 113 114 /** 115 * Process a new response from the license server. 116 * <p> 117 * This data will be used for computing future policy decisions. The 118 * following parameters are processed: 119 * <ul> 120 * <li>VT: the timestamp that the client should consider the response valid 121 * until 122 * <li>GT: the timestamp that the client should ignore retry errors until 123 * <li>GR: the number of retry errors that the client should ignore 124 * </ul> 125 * 126 * @param response the result from validating the server response 127 * @param rawData the raw server response data 128 */ processServerResponse(int response, com.google.android.vending.licensing.ResponseData rawData)129 public void processServerResponse(int response, 130 com.google.android.vending.licensing.ResponseData rawData) { 131 132 // Update retry counter 133 if (response != Policy.RETRY) { 134 setRetryCount(0); 135 } else { 136 setRetryCount(mRetryCount + 1); 137 } 138 139 if (response == Policy.LICENSED) { 140 // Update server policy data 141 Map<String, String> extras = decodeExtras(rawData.extra); 142 mLastResponse = response; 143 setValidityTimestamp(Long.toString(System.currentTimeMillis() + MILLIS_PER_MINUTE)); 144 Set<String> keys = extras.keySet(); 145 for (String key : keys) { 146 if (key.equals("VT")) { 147 setValidityTimestamp(extras.get(key)); 148 } else if (key.equals("GT")) { 149 setRetryUntil(extras.get(key)); 150 } else if (key.equals("GR")) { 151 setMaxRetries(extras.get(key)); 152 } else if (key.startsWith("FILE_URL")) { 153 int index = Integer.parseInt(key.substring("FILE_URL".length())) - 1; 154 setExpansionURL(index, extras.get(key)); 155 } else if (key.startsWith("FILE_NAME")) { 156 int index = Integer.parseInt(key.substring("FILE_NAME".length())) - 1; 157 setExpansionFileName(index, extras.get(key)); 158 } else if (key.startsWith("FILE_SIZE")) { 159 int index = Integer.parseInt(key.substring("FILE_SIZE".length())) - 1; 160 setExpansionFileSize(index, Long.parseLong(extras.get(key))); 161 } 162 } 163 } else if (response == Policy.NOT_LICENSED) { 164 // Clear out stale policy data 165 setValidityTimestamp(DEFAULT_VALIDITY_TIMESTAMP); 166 setRetryUntil(DEFAULT_RETRY_UNTIL); 167 setMaxRetries(DEFAULT_MAX_RETRIES); 168 } 169 170 setLastResponse(response); 171 mPreferences.commit(); 172 } 173 174 /** 175 * Set the last license response received from the server and add to 176 * preferences. You must manually call PreferenceObfuscator.commit() to 177 * commit these changes to disk. 178 * 179 * @param l the response 180 */ setLastResponse(int l)181 private void setLastResponse(int l) { 182 mLastResponseTime = System.currentTimeMillis(); 183 mLastResponse = l; 184 mPreferences.putString(PREF_LAST_RESPONSE, Integer.toString(l)); 185 } 186 187 /** 188 * Set the current retry count and add to preferences. You must manually 189 * call PreferenceObfuscator.commit() to commit these changes to disk. 190 * 191 * @param c the new retry count 192 */ setRetryCount(long c)193 private void setRetryCount(long c) { 194 mRetryCount = c; 195 mPreferences.putString(PREF_RETRY_COUNT, Long.toString(c)); 196 } 197 getRetryCount()198 public long getRetryCount() { 199 return mRetryCount; 200 } 201 202 /** 203 * Set the last validity timestamp (VT) received from the server and add to 204 * preferences. You must manually call PreferenceObfuscator.commit() to 205 * commit these changes to disk. 206 * 207 * @param validityTimestamp the VT string received 208 */ setValidityTimestamp(String validityTimestamp)209 private void setValidityTimestamp(String validityTimestamp) { 210 Long lValidityTimestamp; 211 try { 212 lValidityTimestamp = Long.parseLong(validityTimestamp); 213 } catch (NumberFormatException e) { 214 // No response or not parseable, expire in one minute. 215 Log.w(TAG, "License validity timestamp (VT) missing, caching for a minute"); 216 lValidityTimestamp = System.currentTimeMillis() + MILLIS_PER_MINUTE; 217 validityTimestamp = Long.toString(lValidityTimestamp); 218 } 219 220 mValidityTimestamp = lValidityTimestamp; 221 mPreferences.putString(PREF_VALIDITY_TIMESTAMP, validityTimestamp); 222 } 223 getValidityTimestamp()224 public long getValidityTimestamp() { 225 return mValidityTimestamp; 226 } 227 228 /** 229 * Set the retry until timestamp (GT) received from the server and add to 230 * preferences. You must manually call PreferenceObfuscator.commit() to 231 * commit these changes to disk. 232 * 233 * @param retryUntil the GT string received 234 */ setRetryUntil(String retryUntil)235 private void setRetryUntil(String retryUntil) { 236 Long lRetryUntil; 237 try { 238 lRetryUntil = Long.parseLong(retryUntil); 239 } catch (NumberFormatException e) { 240 // No response or not parseable, expire immediately 241 Log.w(TAG, "License retry timestamp (GT) missing, grace period disabled"); 242 retryUntil = "0"; 243 lRetryUntil = 0l; 244 } 245 246 mRetryUntil = lRetryUntil; 247 mPreferences.putString(PREF_RETRY_UNTIL, retryUntil); 248 } 249 getRetryUntil()250 public long getRetryUntil() { 251 return mRetryUntil; 252 } 253 254 /** 255 * Set the max retries value (GR) as received from the server and add to 256 * preferences. You must manually call PreferenceObfuscator.commit() to 257 * commit these changes to disk. 258 * 259 * @param maxRetries the GR string received 260 */ setMaxRetries(String maxRetries)261 private void setMaxRetries(String maxRetries) { 262 Long lMaxRetries; 263 try { 264 lMaxRetries = Long.parseLong(maxRetries); 265 } catch (NumberFormatException e) { 266 // No response or not parseable, expire immediately 267 Log.w(TAG, "Licence retry count (GR) missing, grace period disabled"); 268 maxRetries = "0"; 269 lMaxRetries = 0l; 270 } 271 272 mMaxRetries = lMaxRetries; 273 mPreferences.putString(PREF_MAX_RETRIES, maxRetries); 274 } 275 getMaxRetries()276 public long getMaxRetries() { 277 return mMaxRetries; 278 } 279 280 /** 281 * Gets the count of expansion URLs. Since expansionURLs are not committed 282 * to preferences, this will return zero if there has been no LVL fetch 283 * in the current session. 284 * 285 * @return the number of expansion URLs. (0,1,2) 286 */ getExpansionURLCount()287 public int getExpansionURLCount() { 288 return mExpansionURLs.size(); 289 } 290 291 /** 292 * Gets the expansion URL. Since these URLs are not committed to 293 * preferences, this will always return null if there has not been an LVL 294 * fetch in the current session. 295 * 296 * @param index the index of the URL to fetch. This value will be either 297 * MAIN_FILE_URL_INDEX or PATCH_FILE_URL_INDEX 298 * @param URL the URL to set 299 */ getExpansionURL(int index)300 public String getExpansionURL(int index) { 301 if (index < mExpansionURLs.size()) { 302 return mExpansionURLs.elementAt(index); 303 } 304 return null; 305 } 306 307 /** 308 * Sets the expansion URL. Expansion URL's are not committed to preferences, 309 * but are instead intended to be stored when the license response is 310 * processed by the front-end. 311 * 312 * @param index the index of the expansion URL. This value will be either 313 * MAIN_FILE_URL_INDEX or PATCH_FILE_URL_INDEX 314 * @param URL the URL to set 315 */ setExpansionURL(int index, String URL)316 public void setExpansionURL(int index, String URL) { 317 if (index >= mExpansionURLs.size()) { 318 mExpansionURLs.setSize(index + 1); 319 } 320 mExpansionURLs.set(index, URL); 321 } 322 getExpansionFileName(int index)323 public String getExpansionFileName(int index) { 324 if (index < mExpansionFileNames.size()) { 325 return mExpansionFileNames.elementAt(index); 326 } 327 return null; 328 } 329 setExpansionFileName(int index, String name)330 public void setExpansionFileName(int index, String name) { 331 if (index >= mExpansionFileNames.size()) { 332 mExpansionFileNames.setSize(index + 1); 333 } 334 mExpansionFileNames.set(index, name); 335 } 336 getExpansionFileSize(int index)337 public long getExpansionFileSize(int index) { 338 if (index < mExpansionFileSizes.size()) { 339 return mExpansionFileSizes.elementAt(index); 340 } 341 return -1; 342 } 343 setExpansionFileSize(int index, long size)344 public void setExpansionFileSize(int index, long size) { 345 if (index >= mExpansionFileSizes.size()) { 346 mExpansionFileSizes.setSize(index + 1); 347 } 348 mExpansionFileSizes.set(index, size); 349 } 350 351 /** 352 * {@inheritDoc} This implementation allows access if either:<br> 353 * <ol> 354 * <li>a LICENSED response was received within the validity period 355 * <li>a RETRY response was received in the last minute, and we are under 356 * the RETRY count or in the RETRY period. 357 * </ol> 358 */ allowAccess()359 public boolean allowAccess() { 360 long ts = System.currentTimeMillis(); 361 if (mLastResponse == Policy.LICENSED) { 362 // Check if the LICENSED response occurred within the validity 363 // timeout. 364 if (ts <= mValidityTimestamp) { 365 // Cached LICENSED response is still valid. 366 return true; 367 } 368 } else if (mLastResponse == Policy.RETRY && 369 ts < mLastResponseTime + MILLIS_PER_MINUTE) { 370 // Only allow access if we are within the retry period or we haven't 371 // used up our 372 // max retries. 373 return (ts <= mRetryUntil || mRetryCount <= mMaxRetries); 374 } 375 return false; 376 } 377 decodeExtras(String extras)378 private Map<String, String> decodeExtras(String extras) { 379 Map<String, String> results = new HashMap<String, String>(); 380 try { 381 URI rawExtras = new URI("?" + extras); 382 List<NameValuePair> extraList = URLEncodedUtils.parse(rawExtras, "UTF-8"); 383 for (NameValuePair item : extraList) { 384 String name = item.getName(); 385 int i = 0; 386 while (results.containsKey(name)) { 387 name = item.getName() + ++i; 388 } 389 results.put(name, item.getValue()); 390 } 391 } catch (URISyntaxException e) { 392 Log.w(TAG, "Invalid syntax error while decoding extras data from server."); 393 } 394 return results; 395 } 396 397 } 398