1/*
2 * Copyright (C) 2011 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.volley;
18
19import android.os.Handler;
20import android.os.Looper;
21import android.os.SystemClock;
22
23import com.android.volley.VolleyLog.MarkerLog;
24
25import java.io.UnsupportedEncodingException;
26import java.net.URLEncoder;
27import java.util.Collections;
28import java.util.Map;
29
30/**
31 * Base class for all network requests.
32 *
33 * @param <T> The type of parsed response this request expects.
34 */
35public abstract class Request<T> implements Comparable<Request<T>> {
36
37    /** Default encoding for POST parameters. See {@link #getPostParamsEncoding()}. */
38    private static final String DEFAULT_POST_PARAMS_ENCODING = "UTF-8";
39
40    /** An event log tracing the lifetime of this request; for debugging. */
41    private final MarkerLog mEventLog = MarkerLog.ENABLED ? new MarkerLog() : null;
42
43    /** URL of this request. */
44    private final String mUrl;
45
46    /** Listener interface for errors. */
47    private final Response.ErrorListener mErrorListener;
48
49    /** Sequence number of this request, used to enforce FIFO ordering. */
50    private Integer mSequence;
51
52    /** The request queue this request is associated with. */
53    private RequestQueue mRequestQueue;
54
55    /** Whether or not responses to this request should be cached. */
56    private boolean mShouldCache = true;
57
58    /** Whether or not this request has been canceled. */
59    private boolean mCanceled = false;
60
61    /** Whether or not a response has been delivered for this request yet. */
62    private boolean mResponseDelivered = false;
63
64    // A cheap variant of request tracing used to dump slow requests.
65    private long mRequestBirthTime = 0;
66
67    /** Threshold at which we should log the request (even when debug logging is not enabled). */
68    private static final long SLOW_REQUEST_THRESHOLD_MS = 3000;
69
70    /** The retry policy for this request. */
71    private RetryPolicy mRetryPolicy;
72
73    /**
74     * When a request can be retrieved from cache but must be refreshed from
75     * the network, the cache entry will be stored here so that in the event of
76     * a "Not Modified" response, we can be sure it hasn't been evicted from cache.
77     */
78    private Cache.Entry mCacheEntry = null;
79
80    /** An opaque token tagging this request; used for bulk cancellation. */
81    private Object mTag;
82
83    /**
84     * Creates a new request with the given URL and error listener.  Note that
85     * the normal response listener is not provided here as delivery of responses
86     * is provided by subclasses, who have a better idea of how to deliver an
87     * already-parsed response.
88     */
89    public Request(String url, Response.ErrorListener listener) {
90        mUrl = url;
91        mErrorListener = listener;
92        setRetryPolicy(new DefaultRetryPolicy());
93    }
94
95    /**
96     * Set a tag on this request. Can be used to cancel all requests with this
97     * tag by {@link RequestQueue#cancelAll(Object)}.
98     */
99    public void setTag(Object tag) {
100        mTag = tag;
101    }
102
103    /**
104     * Returns this request's tag.
105     * @see Request#setTag(Object)
106     */
107    public Object getTag() {
108        return mTag;
109    }
110
111    /**
112     * Sets the retry policy for this request.
113     */
114    public void setRetryPolicy(RetryPolicy retryPolicy) {
115        mRetryPolicy = retryPolicy;
116    }
117
118    /**
119     * Adds an event to this request's event log; for debugging.
120     */
121    public void addMarker(String tag) {
122        if (MarkerLog.ENABLED) {
123            mEventLog.add(tag, Thread.currentThread().getId());
124        } else if (mRequestBirthTime == 0) {
125            mRequestBirthTime = SystemClock.elapsedRealtime();
126        }
127    }
128
129    /**
130     * Notifies the request queue that this request has finished (successfully or with error).
131     *
132     * <p>Also dumps all events from this request's event log; for debugging.</p>
133     */
134    void finish(final String tag) {
135        if (mRequestQueue != null) {
136            mRequestQueue.finish(this);
137        }
138        if (MarkerLog.ENABLED) {
139            final long threadId = Thread.currentThread().getId();
140            if (Looper.myLooper() != Looper.getMainLooper()) {
141                // If we finish marking off of the main thread, we need to
142                // actually do it on the main thread to ensure correct ordering.
143                Handler mainThread = new Handler(Looper.getMainLooper());
144                mainThread.post(new Runnable() {
145                    @Override
146                    public void run() {
147                        mEventLog.add(tag, threadId);
148                        mEventLog.finish(this.toString());
149                    }
150                });
151                return;
152            }
153
154            mEventLog.add(tag, threadId);
155            mEventLog.finish(this.toString());
156        } else {
157            long requestTime = SystemClock.elapsedRealtime() - mRequestBirthTime;
158            if (requestTime >= SLOW_REQUEST_THRESHOLD_MS) {
159                VolleyLog.d("%d ms: %s", requestTime, this.toString());
160            }
161        }
162    }
163
164    /**
165     * Associates this request with the given queue. The request queue will be notified when this
166     * request has finished.
167     */
168    public void setRequestQueue(RequestQueue requestQueue) {
169        mRequestQueue = requestQueue;
170    }
171
172    /**
173     * Sets the sequence number of this request.  Used by {@link RequestQueue}.
174     */
175    public final void setSequence(int sequence) {
176        mSequence = sequence;
177    }
178
179    /**
180     * Returns the sequence number of this request.
181     */
182    public final int getSequence() {
183        if (mSequence == null) {
184            throw new IllegalStateException("getSequence called before setSequence");
185        }
186        return mSequence;
187    }
188
189    /**
190     * Returns the URL of this request.
191     */
192    public String getUrl() {
193        return mUrl;
194    }
195
196    /**
197     * Returns the cache key for this request.  By default, this is the URL.
198     */
199    public String getCacheKey() {
200        return getUrl();
201    }
202
203    /**
204     * Annotates this request with an entry retrieved for it from cache.
205     * Used for cache coherency support.
206     */
207    public void setCacheEntry(Cache.Entry entry) {
208        mCacheEntry = entry;
209    }
210
211    /**
212     * Returns the annotated cache entry, or null if there isn't one.
213     */
214    public Cache.Entry getCacheEntry() {
215        return mCacheEntry;
216    }
217
218    /**
219     * Mark this request as canceled.  No callback will be delivered.
220     */
221    public void cancel() {
222        mCanceled = true;
223    }
224
225    /**
226     * Returns true if this request has been canceled.
227     */
228    public boolean isCanceled() {
229        return mCanceled;
230    }
231
232    /**
233     * Returns a list of extra HTTP headers to go along with this request. Can
234     * throw {@link AuthFailureError} as authentication may be required to
235     * provide these values.
236     * @throws AuthFailureError In the event of auth failure
237     */
238    public Map<String, String> getHeaders() throws AuthFailureError {
239        return Collections.emptyMap();
240    }
241
242    /**
243     * Returns a Map of POST parameters to be used for this request, or null if
244     * a simple GET should be used.  Can throw {@link AuthFailureError} as
245     * authentication may be required to provide these values.
246     *
247     * <p>Note that only one of getPostParams() and getPostBody() can return a non-null
248     * value.</p>
249     * @throws AuthFailureError In the event of auth failure
250     */
251    protected Map<String, String> getPostParams() throws AuthFailureError {
252        return null;
253    }
254
255    /**
256     * Returns which encoding should be used when converting POST parameters returned by
257     * {@link #getPostParams()} into a raw POST body.
258     *
259     * <p>This controls both encodings:
260     * <ol>
261     *     <li>The string encoding used when converting parameter names and values into bytes prior
262     *         to URL encoding them.</li>
263     *     <li>The string encoding used when converting the URL encoded parameters into a raw
264     *         byte array.</li>
265     * </ol>
266     */
267    protected String getPostParamsEncoding() {
268        return DEFAULT_POST_PARAMS_ENCODING;
269    }
270
271    public String getPostBodyContentType() {
272        return "application/x-www-form-urlencoded; charset=" + getPostParamsEncoding();
273    }
274
275    /**
276     * Returns the raw POST body to be sent.
277     *
278     * @throws AuthFailureError In the event of auth failure
279     */
280    public byte[] getPostBody() throws AuthFailureError {
281        Map<String, String> postParams = getPostParams();
282        if (postParams != null && postParams.size() > 0) {
283            return encodePostParameters(postParams, getPostParamsEncoding());
284        }
285        return null;
286    }
287
288    /**
289     * Converts <code>postParams</code> into an application/x-www-form-urlencoded encoded string.
290     */
291    private byte[] encodePostParameters(Map<String, String> postParams, String postParamsEncoding) {
292        StringBuilder encodedParams = new StringBuilder();
293        try {
294            for (Map.Entry<String, String> entry : postParams.entrySet()) {
295                encodedParams.append(URLEncoder.encode(entry.getKey(), postParamsEncoding));
296                encodedParams.append('=');
297                encodedParams.append(URLEncoder.encode(entry.getValue(), postParamsEncoding));
298                encodedParams.append('&');
299            }
300            return encodedParams.toString().getBytes(postParamsEncoding);
301        } catch (UnsupportedEncodingException uee) {
302            throw new RuntimeException("Encoding not supported: " + postParamsEncoding, uee);
303        }
304    }
305
306    /**
307     * Set whether or not responses to this request should be cached.
308     */
309    public final void setShouldCache(boolean shouldCache) {
310        mShouldCache = shouldCache;
311    }
312
313    /**
314     * Returns true if responses to this request should be cached.
315     */
316    public final boolean shouldCache() {
317        return mShouldCache;
318    }
319
320    /**
321     * Priority values.  Requests will be processed from higher priorities to
322     * lower priorities, in FIFO order.
323     */
324    public enum Priority {
325        LOW,
326        NORMAL,
327        HIGH,
328        IMMEDIATE
329    }
330
331    /**
332     * Returns the {@link Priority} of this request; {@link Priority#NORMAL} by default.
333     */
334    public Priority getPriority() {
335        return Priority.NORMAL;
336    }
337
338    /**
339     * Returns the socket timeout in milliseconds per retry attempt. (This value can be changed
340     * per retry attempt if a backoff is specified via backoffTimeout()). If there are no retry
341     * attempts remaining, this will cause delivery of a {@link TimeoutError} error.
342     */
343    public final int getTimeoutMs() {
344        return mRetryPolicy.getCurrentTimeout();
345    }
346
347    /**
348     * Returns the retry policy that should be used  for this request.
349     */
350    public RetryPolicy getRetryPolicy() {
351        return mRetryPolicy;
352    }
353
354    /**
355     * Mark this request as having a response delivered on it.  This can be used
356     * later in the request's lifetime for suppressing identical responses.
357     */
358    public void markDelivered() {
359        mResponseDelivered = true;
360    }
361
362    /**
363     * Returns true if this request has had a response delivered for it.
364     */
365    public boolean hasHadResponseDelivered() {
366        return mResponseDelivered;
367    }
368
369    /**
370     * Subclasses must implement this to parse the raw network response
371     * and return an appropriate response type. This method will be
372     * called from a worker thread.  The response will not be delivered
373     * if you return null.
374     * @param response Response from the network
375     * @return The parsed response, or null in the case of an error
376     */
377    abstract protected Response<T> parseNetworkResponse(NetworkResponse response);
378
379    /**
380     * Subclasses can override this method to parse 'networkError' and return a more specific error.
381     *
382     * <p>The default implementation just returns the passed 'networkError'.</p>
383     *
384     * @param volleyError the error retrieved from the network
385     * @return an NetworkError augmented with additional information
386     */
387    protected VolleyError parseNetworkError(VolleyError volleyError) {
388        return volleyError;
389    }
390
391    /**
392     * Subclasses must implement this to perform delivery of the parsed
393     * response to their listeners.  The given response is guaranteed to
394     * be non-null; responses that fail to parse are not delivered.
395     * @param response The parsed response returned by
396     * {@link #parseNetworkResponse(NetworkResponse)}
397     */
398    abstract protected void deliverResponse(T response);
399
400    /**
401     * Delivers error message to the ErrorListener that the Request was
402     * initialized with.
403     *
404     * @param error Error details
405     */
406    public void deliverError(VolleyError error) {
407        if (mErrorListener != null) {
408            mErrorListener.onErrorResponse(error);
409        }
410    }
411
412    /**
413     * Our comparator sorts from high to low priority, and secondarily by
414     * sequence number to provide FIFO ordering.
415     */
416    @Override
417    public int compareTo(Request<T> other) {
418        Priority left = this.getPriority();
419        Priority right = other.getPriority();
420
421        // High-priority requests are "lesser" so they are sorted to the front.
422        // Equal priorities are sorted by sequence number to provide FIFO ordering.
423        return left == right ?
424                this.mSequence - other.mSequence :
425                right.ordinal() - left.ordinal();
426    }
427
428    @Override
429    public String toString() {
430        return (mCanceled ? "[X] " : "[ ] ") + getUrl() + " " + getPriority() + " " + mSequence;
431    }
432}
433