aboutsummaryrefslogtreecommitdiff
path: root/app/src/main/java/moe/yuuta/gplicense/ServerManagedPolicy.java
blob: fcc3451d8567eb45dd45483df969bba36a641a52 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
package moe.yuuta.gplicense;

import android.content.Context;
import android.content.SharedPreferences;

import com.elvishew.xlog.Logger;
import com.elvishew.xlog.XLog;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Map;

import moe.yuuta.gplicense.util.URIQueryDecoder;

/**
 * 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 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 context 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.
     * <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, 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(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:<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(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) {
            logger.w("Invalid syntax error while decoding extras data from server.");
        }
        return results;
    }

}