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