mirror of https://github.com/kurisufriend/Clover
commit
9efd5d6803
@ -1,61 +0,0 @@ |
||||
/* |
||||
* Copyright (C) 2011 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.android.volley; |
||||
|
||||
import android.content.Intent; |
||||
|
||||
import com.android.volley.NetworkResponse; |
||||
import com.android.volley.VolleyError; |
||||
|
||||
/** |
||||
* Error indicating that there was an authentication failure when performing a Request. |
||||
*/ |
||||
@SuppressWarnings("serial") |
||||
public class AuthFailureError extends VolleyError { |
||||
/** An intent that can be used to resolve this exception. (Brings up the password dialog.) */ |
||||
private Intent mResolutionIntent; |
||||
|
||||
public AuthFailureError() { } |
||||
|
||||
public AuthFailureError(Intent intent) { |
||||
mResolutionIntent = intent; |
||||
} |
||||
|
||||
public AuthFailureError(NetworkResponse response) { |
||||
super(response); |
||||
} |
||||
|
||||
public AuthFailureError(String message) { |
||||
super(message); |
||||
} |
||||
|
||||
public AuthFailureError(String message, Exception reason) { |
||||
super(message, reason); |
||||
} |
||||
|
||||
public Intent getResolutionIntent() { |
||||
return mResolutionIntent; |
||||
} |
||||
|
||||
@Override |
||||
public String getMessage() { |
||||
if (mResolutionIntent != null) { |
||||
return "User needs to (re)enter credentials."; |
||||
} |
||||
return super.getMessage(); |
||||
} |
||||
} |
@ -1,97 +0,0 @@ |
||||
/* |
||||
* Copyright (C) 2011 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.android.volley; |
||||
|
||||
import java.util.Collections; |
||||
import java.util.Map; |
||||
|
||||
/** |
||||
* An interface for a cache keyed by a String with a byte array as data. |
||||
*/ |
||||
public interface Cache { |
||||
/** |
||||
* Retrieves an entry from the cache. |
||||
* @param key Cache key |
||||
* @return An {@link Entry} or null in the event of a cache miss |
||||
*/ |
||||
public Entry get(String key); |
||||
|
||||
/** |
||||
* Adds or replaces an entry to the cache. |
||||
* @param key Cache key |
||||
* @param entry Data to store and metadata for cache coherency, TTL, etc. |
||||
*/ |
||||
public void put(String key, Entry entry); |
||||
|
||||
/** |
||||
* Performs any potentially long-running actions needed to initialize the cache; |
||||
* will be called from a worker thread. |
||||
*/ |
||||
public void initialize(); |
||||
|
||||
/** |
||||
* Invalidates an entry in the cache. |
||||
* @param key Cache key |
||||
* @param fullExpire True to fully expire the entry, false to soft expire |
||||
*/ |
||||
public void invalidate(String key, boolean fullExpire); |
||||
|
||||
/** |
||||
* Removes an entry from the cache. |
||||
* @param key Cache key |
||||
*/ |
||||
public void remove(String key); |
||||
|
||||
/** |
||||
* Empties the cache. |
||||
*/ |
||||
public void clear(); |
||||
|
||||
/** |
||||
* Data and metadata for an entry returned by the cache. |
||||
*/ |
||||
public static class Entry { |
||||
/** The data returned from cache. */ |
||||
public byte[] data; |
||||
|
||||
/** ETag for cache coherency. */ |
||||
public String etag; |
||||
|
||||
/** Date of this response as reported by the server. */ |
||||
public long serverDate; |
||||
|
||||
/** TTL for this record. */ |
||||
public long ttl; |
||||
|
||||
/** Soft TTL for this record. */ |
||||
public long softTtl; |
||||
|
||||
/** Immutable response headers as received from server; must be non-null. */ |
||||
public Map<String, String> responseHeaders = Collections.emptyMap(); |
||||
|
||||
/** True if the entry is expired. */ |
||||
public boolean isExpired() { |
||||
return this.ttl < System.currentTimeMillis(); |
||||
} |
||||
|
||||
/** True if a refresh is needed from the original data source. */ |
||||
public boolean refreshNeeded() { |
||||
return this.softTtl < System.currentTimeMillis(); |
||||
} |
||||
} |
||||
|
||||
} |
@ -1,158 +0,0 @@ |
||||
/* |
||||
* Copyright (C) 2011 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.android.volley; |
||||
|
||||
import android.os.Process; |
||||
|
||||
import java.util.concurrent.BlockingQueue; |
||||
|
||||
/** |
||||
* Provides a thread for performing cache triage on a queue of requests. |
||||
* |
||||
* Requests added to the specified cache queue are resolved from cache. |
||||
* Any deliverable response is posted back to the caller via a |
||||
* {@link ResponseDelivery}. Cache misses and responses that require |
||||
* refresh are enqueued on the specified network queue for processing |
||||
* by a {@link NetworkDispatcher}. |
||||
*/ |
||||
public class CacheDispatcher extends Thread { |
||||
|
||||
private static final boolean DEBUG = VolleyLog.DEBUG; |
||||
|
||||
/** The queue of requests coming in for triage. */ |
||||
private final BlockingQueue<Request<?>> mCacheQueue; |
||||
|
||||
/** The queue of requests going out to the network. */ |
||||
private final BlockingQueue<Request<?>> mNetworkQueue; |
||||
|
||||
/** The cache to read from. */ |
||||
private final Cache mCache; |
||||
|
||||
/** For posting responses. */ |
||||
private final ResponseDelivery mDelivery; |
||||
|
||||
/** Used for telling us to die. */ |
||||
private volatile boolean mQuit = false; |
||||
|
||||
/** |
||||
* Creates a new cache triage dispatcher thread. You must call {@link #start()} |
||||
* in order to begin processing. |
||||
* |
||||
* @param cacheQueue Queue of incoming requests for triage |
||||
* @param networkQueue Queue to post requests that require network to |
||||
* @param cache Cache interface to use for resolution |
||||
* @param delivery Delivery interface to use for posting responses |
||||
*/ |
||||
public CacheDispatcher( |
||||
BlockingQueue<Request<?>> cacheQueue, BlockingQueue<Request<?>> networkQueue, |
||||
Cache cache, ResponseDelivery delivery) { |
||||
mCacheQueue = cacheQueue; |
||||
mNetworkQueue = networkQueue; |
||||
mCache = cache; |
||||
mDelivery = delivery; |
||||
} |
||||
|
||||
/** |
||||
* Forces this dispatcher to quit immediately. If any requests are still in |
||||
* the queue, they are not guaranteed to be processed. |
||||
*/ |
||||
public void quit() { |
||||
mQuit = true; |
||||
interrupt(); |
||||
} |
||||
|
||||
@Override |
||||
public void run() { |
||||
if (DEBUG) VolleyLog.v("start new dispatcher"); |
||||
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); |
||||
|
||||
// Make a blocking call to initialize the cache.
|
||||
mCache.initialize(); |
||||
|
||||
while (true) { |
||||
try { |
||||
// Get a request from the cache triage queue, blocking until
|
||||
// at least one is available.
|
||||
final Request<?> request = mCacheQueue.take(); |
||||
request.addMarker("cache-queue-take"); |
||||
|
||||
// If the request has been canceled, don't bother dispatching it.
|
||||
if (request.isCanceled()) { |
||||
request.finish("cache-discard-canceled"); |
||||
continue; |
||||
} |
||||
|
||||
// Attempt to retrieve this item from cache.
|
||||
Cache.Entry entry = mCache.get(request.getCacheKey()); |
||||
if (entry == null) { |
||||
request.addMarker("cache-miss"); |
||||
// Cache miss; send off to the network dispatcher.
|
||||
mNetworkQueue.put(request); |
||||
continue; |
||||
} |
||||
|
||||
// If it is completely expired, just send it to the network.
|
||||
if (entry.isExpired()) { |
||||
request.addMarker("cache-hit-expired"); |
||||
request.setCacheEntry(entry); |
||||
mNetworkQueue.put(request); |
||||
continue; |
||||
} |
||||
|
||||
// We have a cache hit; parse its data for delivery back to the request.
|
||||
request.addMarker("cache-hit"); |
||||
Response<?> response = request.parseNetworkResponse( |
||||
new NetworkResponse(entry.data, entry.responseHeaders)); |
||||
request.addMarker("cache-hit-parsed"); |
||||
|
||||
if (!entry.refreshNeeded()) { |
||||
// Completely unexpired cache hit. Just deliver the response.
|
||||
mDelivery.postResponse(request, response); |
||||
} else { |
||||
// Soft-expired cache hit. We can deliver the cached response,
|
||||
// but we need to also send the request to the network for
|
||||
// refreshing.
|
||||
request.addMarker("cache-hit-refresh-needed"); |
||||
request.setCacheEntry(entry); |
||||
|
||||
// Mark the response as intermediate.
|
||||
response.intermediate = true; |
||||
|
||||
// Post the intermediate response back to the user and have
|
||||
// the delivery then forward the request along to the network.
|
||||
mDelivery.postResponse(request, response, new Runnable() { |
||||
@Override |
||||
public void run() { |
||||
try { |
||||
mNetworkQueue.put(request); |
||||
} catch (InterruptedException e) { |
||||
// Not much we can do about this.
|
||||
} |
||||
} |
||||
}); |
||||
} |
||||
|
||||
} catch (InterruptedException e) { |
||||
// We may have been interrupted because it was time to quit.
|
||||
if (mQuit) { |
||||
return; |
||||
} |
||||
continue; |
||||
} |
||||
} |
||||
} |
||||
} |
@ -1,98 +0,0 @@ |
||||
/* |
||||
* Copyright (C) 2011 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.android.volley; |
||||
|
||||
/** |
||||
* Default retry policy for requests. |
||||
*/ |
||||
public class DefaultRetryPolicy implements RetryPolicy { |
||||
/** The current timeout in milliseconds. */ |
||||
private int mCurrentTimeoutMs; |
||||
|
||||
/** The current retry count. */ |
||||
private int mCurrentRetryCount; |
||||
|
||||
/** The maximum number of attempts. */ |
||||
private final int mMaxNumRetries; |
||||
|
||||
/** The backoff multiplier for for the policy. */ |
||||
private final float mBackoffMultiplier; |
||||
|
||||
/** The default socket timeout in milliseconds */ |
||||
public static final int DEFAULT_TIMEOUT_MS = 2500; |
||||
|
||||
/** The default number of retries */ |
||||
public static final int DEFAULT_MAX_RETRIES = 1; |
||||
|
||||
/** The default backoff multiplier */ |
||||
public static final float DEFAULT_BACKOFF_MULT = 1f; |
||||
|
||||
/** |
||||
* Constructs a new retry policy using the default timeouts. |
||||
*/ |
||||
public DefaultRetryPolicy() { |
||||
this(DEFAULT_TIMEOUT_MS, DEFAULT_MAX_RETRIES, DEFAULT_BACKOFF_MULT); |
||||
} |
||||
|
||||
/** |
||||
* Constructs a new retry policy. |
||||
* @param initialTimeoutMs The initial timeout for the policy. |
||||
* @param maxNumRetries The maximum number of retries. |
||||
* @param backoffMultiplier Backoff multiplier for the policy. |
||||
*/ |
||||
public DefaultRetryPolicy(int initialTimeoutMs, int maxNumRetries, float backoffMultiplier) { |
||||
mCurrentTimeoutMs = initialTimeoutMs; |
||||
mMaxNumRetries = maxNumRetries; |
||||
mBackoffMultiplier = backoffMultiplier; |
||||
} |
||||
|
||||
/** |
||||
* Returns the current timeout. |
||||
*/ |
||||
@Override |
||||
public int getCurrentTimeout() { |
||||
return mCurrentTimeoutMs; |
||||
} |
||||
|
||||
/** |
||||
* Returns the current retry count. |
||||
*/ |
||||
@Override |
||||
public int getCurrentRetryCount() { |
||||
return mCurrentRetryCount; |
||||
} |
||||
|
||||
/** |
||||
* Prepares for the next retry by applying a backoff to the timeout. |
||||
* @param error The error code of the last attempt. |
||||
*/ |
||||
@Override |
||||
public void retry(VolleyError error) throws VolleyError { |
||||
mCurrentRetryCount++; |
||||
mCurrentTimeoutMs += (mCurrentTimeoutMs * mBackoffMultiplier); |
||||
if (!hasAttemptRemaining()) { |
||||
throw error; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Returns true if this policy has attempts remaining, false otherwise. |
||||
*/ |
||||
protected boolean hasAttemptRemaining() { |
||||
return mCurrentRetryCount <= mMaxNumRetries; |
||||
} |
||||
} |
@ -1,118 +0,0 @@ |
||||
/* |
||||
* Copyright (C) 2011 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.android.volley; |
||||
|
||||
import android.os.Handler; |
||||
|
||||
import java.util.concurrent.Executor; |
||||
|
||||
/** |
||||
* Delivers responses and errors. |
||||
*/ |
||||
public class ExecutorDelivery implements ResponseDelivery { |
||||
/** Used for posting responses, typically to the main thread. */ |
||||
private final Executor mResponsePoster; |
||||
|
||||
/** |
||||
* Creates a new response delivery interface. |
||||
* @param handler {@link Handler} to post responses on |
||||
*/ |
||||
public ExecutorDelivery(final Handler handler) { |
||||
// Make an Executor that just wraps the handler.
|
||||
mResponsePoster = new Executor() { |
||||
@Override |
||||
public void execute(Runnable command) { |
||||
handler.post(command); |
||||
} |
||||
}; |
||||
} |
||||
|
||||
/** |
||||
* Creates a new response delivery interface, mockable version |
||||
* for testing. |
||||
* @param executor For running delivery tasks |
||||
*/ |
||||
public ExecutorDelivery(Executor executor) { |
||||
mResponsePoster = executor; |
||||
} |
||||
|
||||
@Override |
||||
public void postResponse(Request<?> request, Response<?> response) { |
||||
postResponse(request, response, null); |
||||
} |
||||
|
||||
@Override |
||||
public void postResponse(Request<?> request, Response<?> response, Runnable runnable) { |
||||
request.markDelivered(); |
||||
request.addMarker("post-response"); |
||||
mResponsePoster.execute(new ResponseDeliveryRunnable(request, response, runnable)); |
||||
} |
||||
|
||||
@Override |
||||
public void postError(Request<?> request, VolleyError error) { |
||||
request.addMarker("post-error"); |
||||
Response<?> response = Response.error(error); |
||||
mResponsePoster.execute(new ResponseDeliveryRunnable(request, response, null)); |
||||
} |
||||
|
||||
/** |
||||
* A Runnable used for delivering network responses to a listener on the |
||||
* main thread. |
||||
*/ |
||||
@SuppressWarnings("rawtypes") |
||||
private class ResponseDeliveryRunnable implements Runnable { |
||||
private final Request mRequest; |
||||
private final Response mResponse; |
||||
private final Runnable mRunnable; |
||||
|
||||
public ResponseDeliveryRunnable(Request request, Response response, Runnable runnable) { |
||||
mRequest = request; |
||||
mResponse = response; |
||||
mRunnable = runnable; |
||||
} |
||||
|
||||
@SuppressWarnings("unchecked") |
||||
@Override |
||||
public void run() { |
||||
// If this request has canceled, finish it and don't deliver.
|
||||
if (mRequest.isCanceled()) { |
||||
mRequest.finish("canceled-at-delivery"); |
||||
return; |
||||
} |
||||
|
||||
// Deliver a normal response or error, depending.
|
||||
if (mResponse.isSuccess()) { |
||||
mRequest.deliverResponse(mResponse.result); |
||||
} else { |
||||
mRequest.deliverError(mResponse.error); |
||||
} |
||||
|
||||
// If this is an intermediate response, add a marker, otherwise we're done
|
||||
// and the request can be finished.
|
||||
if (mResponse.intermediate) { |
||||
mRequest.addMarker("intermediate-response"); |
||||
} else { |
||||
mRequest.finish("done"); |
||||
} |
||||
|
||||
// If we have been provided a post-delivery runnable, run it.
|
||||
if (mRunnable != null) { |
||||
mRunnable.run(); |
||||
} |
||||
} |
||||
} |
||||
} |
@ -1,30 +0,0 @@ |
||||
/* |
||||
* Copyright (C) 2011 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.android.volley; |
||||
|
||||
/** |
||||
* An interface for performing requests. |
||||
*/ |
||||
public interface Network { |
||||
/** |
||||
* Performs the specified request. |
||||
* @param request Request to process |
||||
* @return A {@link NetworkResponse} with data and caching metadata; will never be null |
||||
* @throws VolleyError on errors |
||||
*/ |
||||
public NetworkResponse performRequest(Request<?> request) throws VolleyError; |
||||
} |
@ -1,147 +0,0 @@ |
||||
/* |
||||
* Copyright (C) 2011 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.android.volley; |
||||
|
||||
import android.annotation.TargetApi; |
||||
import android.net.TrafficStats; |
||||
import android.os.Build; |
||||
import android.os.Process; |
||||
|
||||
import java.util.concurrent.BlockingQueue; |
||||
|
||||
/** |
||||
* Provides a thread for performing network dispatch from a queue of requests. |
||||
* |
||||
* Requests added to the specified queue are processed from the network via a |
||||
* specified {@link Network} interface. Responses are committed to cache, if |
||||
* eligible, using a specified {@link Cache} interface. Valid responses and |
||||
* errors are posted back to the caller via a {@link ResponseDelivery}. |
||||
*/ |
||||
public class NetworkDispatcher extends Thread { |
||||
/** The queue of requests to service. */ |
||||
private final BlockingQueue<Request<?>> mQueue; |
||||
/** The network interface for processing requests. */ |
||||
private final Network mNetwork; |
||||
/** The cache to write to. */ |
||||
private final Cache mCache; |
||||
/** For posting responses and errors. */ |
||||
private final ResponseDelivery mDelivery; |
||||
/** Used for telling us to die. */ |
||||
private volatile boolean mQuit = false; |
||||
|
||||
/** |
||||
* Creates a new network dispatcher thread. You must call {@link #start()} |
||||
* in order to begin processing. |
||||
* |
||||
* @param queue Queue of incoming requests for triage |
||||
* @param network Network interface to use for performing requests |
||||
* @param cache Cache interface to use for writing responses to cache |
||||
* @param delivery Delivery interface to use for posting responses |
||||
*/ |
||||
public NetworkDispatcher(BlockingQueue<Request<?>> queue, |
||||
Network network, Cache cache, |
||||
ResponseDelivery delivery) { |
||||
mQueue = queue; |
||||
mNetwork = network; |
||||
mCache = cache; |
||||
mDelivery = delivery; |
||||
} |
||||
|
||||
/** |
||||
* Forces this dispatcher to quit immediately. If any requests are still in |
||||
* the queue, they are not guaranteed to be processed. |
||||
*/ |
||||
public void quit() { |
||||
mQuit = true; |
||||
interrupt(); |
||||
} |
||||
|
||||
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) |
||||
private void addTrafficStatsTag(Request<?> request) { |
||||
// Tag the request (if API >= 14)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { |
||||
TrafficStats.setThreadStatsTag(request.getTrafficStatsTag()); |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public void run() { |
||||
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); |
||||
while (true) { |
||||
Request<?> request; |
||||
try { |
||||
// Take a request from the queue.
|
||||
request = mQueue.take(); |
||||
} catch (InterruptedException e) { |
||||
// We may have been interrupted because it was time to quit.
|
||||
if (mQuit) { |
||||
return; |
||||
} |
||||
continue; |
||||
} |
||||
|
||||
try { |
||||
request.addMarker("network-queue-take"); |
||||
|
||||
// If the request was cancelled already, do not perform the
|
||||
// network request.
|
||||
if (request.isCanceled()) { |
||||
request.finish("network-discard-cancelled"); |
||||
continue; |
||||
} |
||||
|
||||
addTrafficStatsTag(request); |
||||
|
||||
// Perform the network request.
|
||||
NetworkResponse networkResponse = mNetwork.performRequest(request); |
||||
request.addMarker("network-http-complete"); |
||||
|
||||
// If the server returned 304 AND we delivered a response already,
|
||||
// we're done -- don't deliver a second identical response.
|
||||
if (networkResponse.notModified && request.hasHadResponseDelivered()) { |
||||
request.finish("not-modified"); |
||||
continue; |
||||
} |
||||
|
||||
// Parse the response here on the worker thread.
|
||||
Response<?> response = request.parseNetworkResponse(networkResponse); |
||||
request.addMarker("network-parse-complete"); |
||||
|
||||
// Write to cache if applicable.
|
||||
// TODO: Only update cache metadata instead of entire record for 304s.
|
||||
if (request.shouldCache() && response.cacheEntry != null) { |
||||
mCache.put(request.getCacheKey(), response.cacheEntry); |
||||
request.addMarker("network-cache-written"); |
||||
} |
||||
|
||||
// Post the response back.
|
||||
request.markDelivered(); |
||||
mDelivery.postResponse(request, response); |
||||
} catch (VolleyError volleyError) { |
||||
parseAndDeliverNetworkError(request, volleyError); |
||||
} catch (Exception e) { |
||||
VolleyLog.e(e, "Unhandled exception %s", e.toString()); |
||||
mDelivery.postError(request, new VolleyError(e)); |
||||
} |
||||
} |
||||
} |
||||
|
||||
private void parseAndDeliverNetworkError(Request<?> request, VolleyError error) { |
||||
error = request.parseNetworkError(error); |
||||
mDelivery.postError(request, error); |
||||
} |
||||
} |
@ -1,38 +0,0 @@ |
||||
/* |
||||
* Copyright (C) 2011 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.android.volley; |
||||
|
||||
import com.android.volley.NetworkResponse; |
||||
import com.android.volley.VolleyError; |
||||
|
||||
/** |
||||
* Indicates that there was a network error when performing a Volley request. |
||||
*/ |
||||
@SuppressWarnings("serial") |
||||
public class NetworkError extends VolleyError { |
||||
public NetworkError() { |
||||
super(); |
||||
} |
||||
|
||||
public NetworkError(Throwable cause) { |
||||
super(cause); |
||||
} |
||||
|
||||
public NetworkError(NetworkResponse networkResponse) { |
||||
super(networkResponse); |
||||
} |
||||
} |
@ -1,62 +0,0 @@ |
||||
/* |
||||
* Copyright (C) 2011 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.android.volley; |
||||
|
||||
import org.apache.http.HttpStatus; |
||||
|
||||
import java.util.Collections; |
||||
import java.util.Map; |
||||
|
||||
/** |
||||
* Data and headers returned from {@link Network#performRequest(Request)}. |
||||
*/ |
||||
public class NetworkResponse { |
||||
/** |
||||
* Creates a new network response. |
||||
* @param statusCode the HTTP status code |
||||
* @param data Response body |
||||
* @param headers Headers returned with this response, or null for none |
||||
* @param notModified True if the server returned a 304 and the data was already in cache |
||||
*/ |
||||
public NetworkResponse(int statusCode, byte[] data, Map<String, String> headers, |
||||
boolean notModified) { |
||||
this.statusCode = statusCode; |
||||
this.data = data; |
||||
this.headers = headers; |
||||
this.notModified = notModified; |
||||
} |
||||
|
||||
public NetworkResponse(byte[] data) { |
||||
this(HttpStatus.SC_OK, data, Collections.<String, String>emptyMap(), false); |
||||
} |
||||
|
||||
public NetworkResponse(byte[] data, Map<String, String> headers) { |
||||
this(HttpStatus.SC_OK, data, headers, false); |
||||
} |
||||
|
||||
/** The HTTP status code. */ |
||||
public final int statusCode; |
||||
|
||||
/** Raw data from this response. */ |
||||
public final byte[] data; |
||||
|
||||
/** Response headers. */ |
||||
public final Map<String, String> headers; |
||||
|
||||
/** True if the server returned a 304 (Not Modified). */ |
||||
public final boolean notModified; |
||||
} |
@ -1,31 +0,0 @@ |
||||
/* |
||||
* Copyright (C) 2011 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.android.volley; |
||||
|
||||
/** |
||||
* Error indicating that no connection could be established when performing a Volley request. |
||||
*/ |
||||
@SuppressWarnings("serial") |
||||
public class NoConnectionError extends NetworkError { |
||||
public NoConnectionError() { |
||||
super(); |
||||
} |
||||
|
||||
public NoConnectionError(Throwable reason) { |
||||
super(reason); |
||||
} |
||||
} |
@ -1,36 +0,0 @@ |
||||
/* |
||||
* Copyright (C) 2011 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.android.volley; |
||||
|
||||
import com.android.volley.NetworkResponse; |
||||
import com.android.volley.VolleyError; |
||||
|
||||
/** |
||||
* Indicates that the server's response could not be parsed. |
||||
*/ |
||||
@SuppressWarnings("serial") |
||||
public class ParseError extends VolleyError { |
||||
public ParseError() { } |
||||
|
||||
public ParseError(NetworkResponse networkResponse) { |
||||
super(networkResponse); |
||||
} |
||||
|
||||
public ParseError(Throwable cause) { |
||||
super(cause); |
||||
} |
||||
} |
@ -1,603 +0,0 @@ |
||||
/* |
||||
* Copyright (C) 2011 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.android.volley; |
||||
|
||||
import android.net.TrafficStats; |
||||
import android.net.Uri; |
||||
import android.os.Handler; |
||||
import android.os.Looper; |
||||
import android.os.SystemClock; |
||||
import android.text.TextUtils; |
||||
|
||||
import com.android.volley.VolleyLog.MarkerLog; |
||||
|
||||
import java.io.UnsupportedEncodingException; |
||||
import java.net.URLEncoder; |
||||
import java.util.Collections; |
||||
import java.util.Map; |
||||
|
||||
/** |
||||
* Base class for all network requests. |
||||
* |
||||
* @param <T> The type of parsed response this request expects. |
||||
*/ |
||||
public abstract class Request<T> implements Comparable<Request<T>> { |
||||
|
||||
/** |
||||
* Default encoding for POST or PUT parameters. See {@link #getParamsEncoding()}. |
||||
*/ |
||||
private static final String DEFAULT_PARAMS_ENCODING = "UTF-8"; |
||||
|
||||
/** |
||||
* Supported request methods. |
||||
*/ |
||||
public interface Method { |
||||
int DEPRECATED_GET_OR_POST = -1; |
||||
int GET = 0; |
||||
int POST = 1; |
||||
int PUT = 2; |
||||
int DELETE = 3; |
||||
int HEAD = 4; |
||||
int OPTIONS = 5; |
||||
int TRACE = 6; |
||||
int PATCH = 7; |
||||
} |
||||
|
||||
/** An event log tracing the lifetime of this request; for debugging. */ |
||||
private final MarkerLog mEventLog = MarkerLog.ENABLED ? new MarkerLog() : null; |
||||
|
||||
/** |
||||
* Request method of this request. Currently supports GET, POST, PUT, DELETE, HEAD, OPTIONS, |
||||
* TRACE, and PATCH. |
||||
*/ |
||||
private final int mMethod; |
||||
|
||||
/** URL of this request. */ |
||||
private final String mUrl; |
||||
|
||||
/** Default tag for {@link TrafficStats}. */ |
||||
private final int mDefaultTrafficStatsTag; |
||||
|
||||
/** Listener interface for errors. */ |
||||
private final Response.ErrorListener mErrorListener; |
||||
|
||||
/** Sequence number of this request, used to enforce FIFO ordering. */ |
||||
private Integer mSequence; |
||||
|
||||
/** The request queue this request is associated with. */ |
||||
private RequestQueue mRequestQueue; |
||||
|
||||
/** Whether or not responses to this request should be cached. */ |
||||
private boolean mShouldCache = true; |
||||
|
||||
/** Whether or not this request has been canceled. */ |
||||
private boolean mCanceled = false; |
||||
|
||||
/** Whether or not a response has been delivered for this request yet. */ |
||||
private boolean mResponseDelivered = false; |
||||
|
||||
// A cheap variant of request tracing used to dump slow requests.
|
||||
private long mRequestBirthTime = 0; |
||||
|
||||
/** Threshold at which we should log the request (even when debug logging is not enabled). */ |
||||
private static final long SLOW_REQUEST_THRESHOLD_MS = 3000; |
||||
|
||||
/** The retry policy for this request. */ |
||||
private RetryPolicy mRetryPolicy; |
||||
|
||||
/** |
||||
* When a request can be retrieved from cache but must be refreshed from |
||||
* the network, the cache entry will be stored here so that in the event of |
||||
* a "Not Modified" response, we can be sure it hasn't been evicted from cache. |
||||
*/ |
||||
private Cache.Entry mCacheEntry = null; |
||||
|
||||
/** An opaque token tagging this request; used for bulk cancellation. */ |
||||
private Object mTag; |
||||
|
||||
/** |
||||
* Creates a new request with the given URL and error listener. Note that |
||||
* the normal response listener is not provided here as delivery of responses |
||||
* is provided by subclasses, who have a better idea of how to deliver an |
||||
* already-parsed response. |
||||
* |
||||
* @deprecated Use {@link #Request(int, String, com.android.volley.Response.ErrorListener)}. |
||||
*/ |
||||
@Deprecated |
||||
public Request(String url, Response.ErrorListener listener) { |
||||
this(Method.DEPRECATED_GET_OR_POST, url, listener); |
||||
} |
||||
|
||||
/** |
||||
* Creates a new request with the given method (one of the values from {@link Method}), |
||||
* URL, and error listener. Note that the normal response listener is not provided here as |
||||
* delivery of responses is provided by subclasses, who have a better idea of how to deliver |
||||
* an already-parsed response. |
||||
*/ |
||||
public Request(int method, String url, Response.ErrorListener listener) { |
||||
mMethod = method; |
||||
mUrl = url; |
||||
mErrorListener = listener; |
||||
setRetryPolicy(new DefaultRetryPolicy()); |
||||
|
||||
mDefaultTrafficStatsTag = findDefaultTrafficStatsTag(url); |
||||
} |
||||
|
||||
/** |
||||
* Return the method for this request. Can be one of the values in {@link Method}. |
||||
*/ |
||||
public int getMethod() { |
||||
return mMethod; |
||||
} |
||||
|
||||
/** |
||||
* Set a tag on this request. Can be used to cancel all requests with this |
||||
* tag by {@link RequestQueue#cancelAll(Object)}. |
||||
* |
||||
* @return This Request object to allow for chaining. |
||||
*/ |
||||
public Request<?> setTag(Object tag) { |
||||
mTag = tag; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Returns this request's tag. |
||||
* @see Request#setTag(Object) |
||||
*/ |
||||
public Object getTag() { |
||||
return mTag; |
||||
} |
||||
|
||||
/** |
||||
* @return this request's {@link com.android.volley.Response.ErrorListener}. |
||||
*/ |
||||
public Response.ErrorListener getErrorListener() { |
||||
return mErrorListener; |
||||
} |
||||
|
||||
/** |
||||
* @return A tag for use with {@link TrafficStats#setThreadStatsTag(int)} |
||||
*/ |
||||
public int getTrafficStatsTag() { |
||||
return mDefaultTrafficStatsTag; |
||||
} |
||||
|
||||
/** |
||||
* @return The hashcode of the URL's host component, or 0 if there is none. |
||||
*/ |
||||
private static int findDefaultTrafficStatsTag(String url) { |
||||
if (!TextUtils.isEmpty(url)) { |
||||
Uri uri = Uri.parse(url); |
||||
if (uri != null) { |
||||
String host = uri.getHost(); |
||||
if (host != null) { |
||||
return host.hashCode(); |
||||
} |
||||
} |
||||
} |
||||
return 0; |
||||
} |
||||
|
||||
/** |
||||
* Sets the retry policy for this request. |
||||
* |
||||
* @return This Request object to allow for chaining. |
||||
*/ |
||||
public Request<?> setRetryPolicy(RetryPolicy retryPolicy) { |
||||
mRetryPolicy = retryPolicy; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Adds an event to this request's event log; for debugging. |
||||
*/ |
||||
public void addMarker(String tag) { |
||||
if (MarkerLog.ENABLED) { |
||||
mEventLog.add(tag, Thread.currentThread().getId()); |
||||
} else if (mRequestBirthTime == 0) { |
||||
mRequestBirthTime = SystemClock.elapsedRealtime(); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Notifies the request queue that this request has finished (successfully or with error). |
||||
* |
||||
* <p>Also dumps all events from this request's event log; for debugging.</p> |
||||
*/ |
||||
void finish(final String tag) { |
||||
if (mRequestQueue != null) { |
||||
mRequestQueue.finish(this); |
||||
} |
||||
if (MarkerLog.ENABLED) { |
||||
final long threadId = Thread.currentThread().getId(); |
||||
if (Looper.myLooper() != Looper.getMainLooper()) { |
||||
// If we finish marking off of the main thread, we need to
|
||||
// actually do it on the main thread to ensure correct ordering.
|
||||
Handler mainThread = new Handler(Looper.getMainLooper()); |
||||
mainThread.post(new Runnable() { |
||||
@Override |
||||
public void run() { |
||||
mEventLog.add(tag, threadId); |
||||
mEventLog.finish(this.toString()); |
||||
} |
||||
}); |
||||
return; |
||||
} |
||||
|
||||
mEventLog.add(tag, threadId); |
||||
mEventLog.finish(this.toString()); |
||||
} else { |
||||
long requestTime = SystemClock.elapsedRealtime() - mRequestBirthTime; |
||||
if (requestTime >= SLOW_REQUEST_THRESHOLD_MS) { |
||||
VolleyLog.d("%d ms: %s", requestTime, this.toString()); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Associates this request with the given queue. The request queue will be notified when this |
||||
* request has finished. |
||||
* |
||||
* @return This Request object to allow for chaining. |
||||
*/ |
||||
public Request<?> setRequestQueue(RequestQueue requestQueue) { |
||||
mRequestQueue = requestQueue; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Sets the sequence number of this request. Used by {@link RequestQueue}. |
||||
* |
||||
* @return This Request object to allow for chaining. |
||||
*/ |
||||
public final Request<?> setSequence(int sequence) { |
||||
mSequence = sequence; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Returns the sequence number of this request. |
||||
*/ |
||||
public final int getSequence() { |
||||
if (mSequence == null) { |
||||
throw new IllegalStateException("getSequence called before setSequence"); |
||||
} |
||||
return mSequence; |
||||
} |
||||
|
||||
/** |
||||
* Returns the URL of this request. |
||||
*/ |
||||
public String getUrl() { |
||||
return mUrl; |
||||
} |
||||
|
||||
/** |
||||
* Returns the cache key for this request. By default, this is the URL. |
||||
*/ |
||||
public String getCacheKey() { |
||||
return getUrl(); |
||||
} |
||||
|
||||
/** |
||||
* Annotates this request with an entry retrieved for it from cache. |
||||
* Used for cache coherency support. |
||||
* |
||||
* @return This Request object to allow for chaining. |
||||
*/ |
||||
public Request<?> setCacheEntry(Cache.Entry entry) { |
||||
mCacheEntry = entry; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Returns the annotated cache entry, or null if there isn't one. |
||||
*/ |
||||
public Cache.Entry getCacheEntry() { |
||||
return mCacheEntry; |
||||
} |
||||
|
||||
/** |
||||
* Mark this request as canceled. No callback will be delivered. |
||||
*/ |
||||
public void cancel() { |
||||
mCanceled = true; |
||||
} |
||||
|
||||
/** |
||||
* Returns true if this request has been canceled. |
||||
*/ |
||||
public boolean isCanceled() { |
||||
return mCanceled; |
||||
} |
||||
|
||||
/** |
||||
* Returns a list of extra HTTP headers to go along with this request. Can |
||||
* throw {@link AuthFailureError} as authentication may be required to |
||||
* provide these values. |
||||
* @throws AuthFailureError In the event of auth failure |
||||
*/ |
||||
public Map<String, String> getHeaders() throws AuthFailureError { |
||||
return Collections.emptyMap(); |
||||
} |
||||
|
||||
/** |
||||
* Returns a Map of POST parameters to be used for this request, or null if |
||||
* a simple GET should be used. Can throw {@link AuthFailureError} as |
||||
* authentication may be required to provide these values. |
||||
* |
||||
* <p>Note that only one of getPostParams() and getPostBody() can return a non-null |
||||
* value.</p> |
||||
* @throws AuthFailureError In the event of auth failure |
||||
* |
||||
* @deprecated Use {@link #getParams()} instead. |
||||
*/ |
||||
@Deprecated |
||||
protected Map<String, String> getPostParams() throws AuthFailureError { |
||||
return getParams(); |
||||
} |
||||
|
||||
/** |
||||
* Returns which encoding should be used when converting POST parameters returned by |
||||
* {@link #getPostParams()} into a raw POST body. |
||||
* |
||||
* <p>This controls both encodings: |
||||
* <ol> |
||||
* <li>The string encoding used when converting parameter names and values into bytes prior |
||||
* to URL encoding them.</li> |
||||
* <li>The string encoding used when converting the URL encoded parameters into a raw |
||||
* byte array.</li> |
||||
* </ol> |
||||
* |
||||
* @deprecated Use {@link #getParamsEncoding()} instead. |
||||
*/ |
||||
@Deprecated |
||||
protected String getPostParamsEncoding() { |
||||
return getParamsEncoding(); |
||||
} |
||||
|
||||
/** |
||||
* @deprecated Use {@link #getBodyContentType()} instead. |
||||
*/ |
||||
@Deprecated |
||||
public String getPostBodyContentType() { |
||||
return getBodyContentType(); |
||||
} |
||||
|
||||
/** |
||||
* Returns the raw POST body to be sent. |
||||
* |
||||
* @throws AuthFailureError In the event of auth failure |
||||
* |
||||
* @deprecated Use {@link #getBody()} instead. |
||||
*/ |
||||
@Deprecated |
||||
public byte[] getPostBody() throws AuthFailureError { |
||||
// Note: For compatibility with legacy clients of volley, this implementation must remain
|
||||
// here instead of simply calling the getBody() function because this function must
|
||||
// call getPostParams() and getPostParamsEncoding() since legacy clients would have
|
||||
// overridden these two member functions for POST requests.
|
||||
Map<String, String> postParams = getPostParams(); |
||||
if (postParams != null && postParams.size() > 0) { |
||||
return encodeParameters(postParams, getPostParamsEncoding()); |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
/** |
||||
* Returns a Map of parameters to be used for a POST or PUT request. Can throw |
||||
* {@link AuthFailureError} as authentication may be required to provide these values. |
||||
* |
||||
* <p>Note that you can directly override {@link #getBody()} for custom data.</p> |
||||
* |
||||
* @throws AuthFailureError in the event of auth failure |
||||
*/ |
||||
protected Map<String, String> getParams() throws AuthFailureError { |
||||
return null; |
||||
} |
||||
|
||||
/** |
||||
* Returns which encoding should be used when converting POST or PUT parameters returned by |
||||
* {@link #getParams()} into a raw POST or PUT body. |
||||
* |
||||
* <p>This controls both encodings: |
||||
* <ol> |
||||
* <li>The string encoding used when converting parameter names and values into bytes prior |
||||
* to URL encoding them.</li> |
||||
* <li>The string encoding used when converting the URL encoded parameters into a raw |
||||
* byte array.</li> |
||||
* </ol> |
||||
*/ |
||||
protected String getParamsEncoding() { |
||||
return DEFAULT_PARAMS_ENCODING; |
||||
} |
||||
|
||||
/** |
||||
* Returns the content type of the POST or PUT body. |
||||
*/ |
||||
public String getBodyContentType() { |
||||
return "application/x-www-form-urlencoded; charset=" + getParamsEncoding(); |
||||
} |
||||
|
||||
/** |
||||
* Returns the raw POST or PUT body to be sent. |
||||
* |
||||
* <p>By default, the body consists of the request parameters in |
||||
* application/x-www-form-urlencoded format. When overriding this method, consider overriding |
||||
* {@link #getBodyContentType()} as well to match the new body format. |
||||
* |
||||
* @throws AuthFailureError in the event of auth failure |
||||
*/ |
||||
public byte[] getBody() throws AuthFailureError { |
||||
Map<String, String> params = getParams(); |
||||
if (params != null && params.size() > 0) { |
||||
return encodeParameters(params, getParamsEncoding()); |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
/** |
||||
* Converts <code>params</code> into an application/x-www-form-urlencoded encoded string. |
||||
*/ |
||||
private byte[] encodeParameters(Map<String, String> params, String paramsEncoding) { |
||||
StringBuilder encodedParams = new StringBuilder(); |
||||
try { |
||||
for (Map.Entry<String, String> entry : params.entrySet()) { |
||||
encodedParams.append(URLEncoder.encode(entry.getKey(), paramsEncoding)); |
||||
encodedParams.append('='); |
||||
encodedParams.append(URLEncoder.encode(entry.getValue(), paramsEncoding)); |
||||
encodedParams.append('&'); |
||||
} |
||||
return encodedParams.toString().getBytes(paramsEncoding); |
||||
} catch (UnsupportedEncodingException uee) { |
||||
throw new RuntimeException("Encoding not supported: " + paramsEncoding, uee); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Set whether or not responses to this request should be cached. |
||||
* |
||||
* @return This Request object to allow for chaining. |
||||
*/ |
||||
public final Request<?> setShouldCache(boolean shouldCache) { |
||||
mShouldCache = shouldCache; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Returns true if responses to this request should be cached. |
||||
*/ |
||||
public final boolean shouldCache() { |
||||
return mShouldCache; |
||||
} |
||||
|
||||
/** |
||||
* Priority values. Requests will be processed from higher priorities to |
||||
* lower priorities, in FIFO order. |
||||
*/ |
||||
public enum Priority { |
||||
LOW, |
||||
NORMAL, |
||||
HIGH, |
||||
IMMEDIATE |
||||
} |
||||
|
||||
/** |
||||
* Returns the {@link Priority} of this request; {@link Priority#NORMAL} by default. |
||||
*/ |
||||
public Priority getPriority() { |
||||
return Priority.NORMAL; |
||||
} |
||||
|
||||
/** |
||||
* Returns the socket timeout in milliseconds per retry attempt. (This value can be changed |
||||
* per retry attempt if a backoff is specified via backoffTimeout()). If there are no retry |
||||
* attempts remaining, this will cause delivery of a {@link TimeoutError} error. |
||||
*/ |
||||
public final int getTimeoutMs() { |
||||
return mRetryPolicy.getCurrentTimeout(); |
||||
} |
||||
|
||||
/** |
||||
* Returns the retry policy that should be used for this request. |
||||
*/ |
||||
public RetryPolicy getRetryPolicy() { |
||||
return mRetryPolicy; |
||||
} |
||||
|
||||
/** |
||||
* Mark this request as having a response delivered on it. This can be used |
||||
* later in the request's lifetime for suppressing identical responses. |
||||
*/ |
||||
public void markDelivered() { |
||||
mResponseDelivered = true; |
||||
} |
||||
|
||||
/** |
||||
* Returns true if this request has had a response delivered for it. |
||||
*/ |
||||
public boolean hasHadResponseDelivered() { |
||||
return mResponseDelivered; |
||||
} |
||||
|
||||
/** |
||||
* Subclasses must implement this to parse the raw network response |
||||
* and return an appropriate response type. This method will be |
||||
* called from a worker thread. The response will not be delivered |
||||
* if you return null. |
||||
* @param response Response from the network |
||||
* @return The parsed response, or null in the case of an error |
||||
*/ |
||||
abstract protected Response<T> parseNetworkResponse(NetworkResponse response); |
||||
|
||||
/** |
||||
* Subclasses can override this method to parse 'networkError' and return a more specific error. |
||||
* |
||||
* <p>The default implementation just returns the passed 'networkError'.</p> |
||||
* |
||||
* @param volleyError the error retrieved from the network |
||||
* @return an NetworkError augmented with additional information |
||||
*/ |
||||
protected VolleyError parseNetworkError(VolleyError volleyError) { |
||||
return volleyError; |
||||
} |
||||
|
||||
/** |
||||
* Subclasses must implement this to perform delivery of the parsed |
||||
* response to their listeners. The given response is guaranteed to |
||||
* be non-null; responses that fail to parse are not delivered. |
||||
* @param response The parsed response returned by |
||||
* {@link #parseNetworkResponse(NetworkResponse)} |
||||
*/ |
||||
abstract protected void deliverResponse(T response); |
||||
|
||||
/** |
||||
* Delivers error message to the ErrorListener that the Request was |
||||
* initialized with. |
||||
* |
||||
* @param error Error details |
||||
*/ |
||||
public void deliverError(VolleyError error) { |
||||
if (mErrorListener != null) { |
||||
mErrorListener.onErrorResponse(error); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Our comparator sorts from high to low priority, and secondarily by |
||||
* sequence number to provide FIFO ordering. |
||||
*/ |
||||
@Override |
||||
public int compareTo(Request<T> other) { |
||||
Priority left = this.getPriority(); |
||||
Priority right = other.getPriority(); |
||||
|
||||
// High-priority requests are "lesser" so they are sorted to the front.
|
||||
// Equal priorities are sorted by sequence number to provide FIFO ordering.
|
||||
return left == right ? |
||||
this.mSequence - other.mSequence : |
||||
right.ordinal() - left.ordinal(); |
||||
} |
||||
|
||||
@Override |
||||
public String toString() { |
||||
String trafficStatsTag = "0x" + Integer.toHexString(getTrafficStatsTag()); |
||||
return (mCanceled ? "[X] " : "[ ] ") + getUrl() + " " + trafficStatsTag + " " |
||||
+ getPriority() + " " + mSequence; |
||||
} |
||||
} |
@ -1,286 +0,0 @@ |
||||
/* |
||||
* Copyright (C) 2011 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.android.volley; |
||||
|
||||
import android.os.Handler; |
||||
import android.os.Looper; |
||||
|
||||
import java.util.HashMap; |
||||
import java.util.HashSet; |
||||
import java.util.LinkedList; |
||||
import java.util.Map; |
||||
import java.util.Queue; |
||||
import java.util.Set; |
||||
import java.util.concurrent.PriorityBlockingQueue; |
||||
import java.util.concurrent.atomic.AtomicInteger; |
||||
|
||||
/** |
||||
* A request dispatch queue with a thread pool of dispatchers. |
||||
* |
||||
* Calling {@link #add(Request)} will enqueue the given Request for dispatch, |
||||
* resolving from either cache or network on a worker thread, and then delivering |
||||
* a parsed response on the main thread. |
||||
*/ |
||||
public class RequestQueue { |
||||
|
||||
/** Used for generating monotonically-increasing sequence numbers for requests. */ |
||||
private AtomicInteger mSequenceGenerator = new AtomicInteger(); |
||||
|
||||
/** |
||||
* Staging area for requests that already have a duplicate request in flight. |
||||
* |
||||
* <ul> |
||||
* <li>containsKey(cacheKey) indicates that there is a request in flight for the given cache |
||||
* key.</li> |
||||
* <li>get(cacheKey) returns waiting requests for the given cache key. The in flight request |
||||
* is <em>not</em> contained in that list. Is null if no requests are staged.</li> |
||||
* </ul> |
||||
*/ |
||||
private final Map<String, Queue<Request<?>>> mWaitingRequests = |
||||
new HashMap<String, Queue<Request<?>>>(); |
||||
|
||||
/** |
||||
* The set of all requests currently being processed by this RequestQueue. A Request |
||||
* will be in this set if it is waiting in any queue or currently being processed by |
||||
* any dispatcher. |
||||
*/ |
||||
private final Set<Request<?>> mCurrentRequests = new HashSet<Request<?>>(); |
||||
|
||||
/** The cache triage queue. */ |
||||
private final PriorityBlockingQueue<Request<?>> mCacheQueue = |
||||
new PriorityBlockingQueue<Request<?>>(); |
||||
|
||||
/** The queue of requests that are actually going out to the network. */ |
||||
private final PriorityBlockingQueue<Request<?>> mNetworkQueue = |
||||
new PriorityBlockingQueue<Request<?>>(); |
||||
|
||||
/** Number of network request dispatcher threads to start. */ |
||||
private static final int DEFAULT_NETWORK_THREAD_POOL_SIZE = 4; |
||||
|
||||
/** Cache interface for retrieving and storing responses. */ |
||||
private final Cache mCache; |
||||
|
||||
/** Network interface for performing requests. */ |
||||
private final Network mNetwork; |
||||
|
||||
/** Response delivery mechanism. */ |
||||
private final ResponseDelivery mDelivery; |
||||
|
||||
/** The network dispatchers. */ |
||||
private NetworkDispatcher[] mDispatchers; |
||||
|
||||
/** The cache dispatcher. */ |
||||
private CacheDispatcher mCacheDispatcher; |
||||
|
||||
/** |
||||
* Creates the worker pool. Processing will not begin until {@link #start()} is called. |
||||
* |
||||
* @param cache A Cache to use for persisting responses to disk |
||||
* @param network A Network interface for performing HTTP requests |
||||
* @param threadPoolSize Number of network dispatcher threads to create |
||||
* @param delivery A ResponseDelivery interface for posting responses and errors |
||||
*/ |
||||
public RequestQueue(Cache cache, Network network, int threadPoolSize, |
||||
ResponseDelivery delivery) { |
||||
mCache = cache; |
||||
mNetwork = network; |
||||
mDispatchers = new NetworkDispatcher[threadPoolSize]; |
||||
mDelivery = delivery; |
||||
} |
||||
|
||||
/** |
||||
* Creates the worker pool. Processing will not begin until {@link #start()} is called. |
||||
* |
||||
* @param cache A Cache to use for persisting responses to disk |
||||
* @param network A Network interface for performing HTTP requests |
||||
* @param threadPoolSize Number of network dispatcher threads to create |
||||
*/ |
||||
public RequestQueue(Cache cache, Network network, int threadPoolSize) { |
||||
this(cache, network, threadPoolSize, |
||||
new ExecutorDelivery(new Handler(Looper.getMainLooper()))); |
||||
} |
||||
|
||||
/** |
||||
* Creates the worker pool. Processing will not begin until {@link #start()} is called. |
||||
* |
||||
* @param cache A Cache to use for persisting responses to disk |
||||
* @param network A Network interface for performing HTTP requests |
||||
*/ |
||||
public RequestQueue(Cache cache, Network network) { |
||||
this(cache, network, DEFAULT_NETWORK_THREAD_POOL_SIZE); |
||||
} |
||||
|
||||
/** |
||||
* Starts the dispatchers in this queue. |
||||
*/ |
||||
public void start() { |
||||
stop(); // Make sure any currently running dispatchers are stopped.
|
||||
// Create the cache dispatcher and start it.
|
||||
mCacheDispatcher = new CacheDispatcher(mCacheQueue, mNetworkQueue, mCache, mDelivery); |
||||
mCacheDispatcher.start(); |
||||
|
||||
// Create network dispatchers (and corresponding threads) up to the pool size.
|
||||
for (int i = 0; i < mDispatchers.length; i++) { |
||||
NetworkDispatcher networkDispatcher = new NetworkDispatcher(mNetworkQueue, mNetwork, |
||||
mCache, mDelivery); |
||||
mDispatchers[i] = networkDispatcher; |
||||
networkDispatcher.start(); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Stops the cache and network dispatchers. |
||||
*/ |
||||
public void stop() { |
||||
if (mCacheDispatcher != null) { |
||||
mCacheDispatcher.quit(); |
||||
} |
||||
for (int i = 0; i < mDispatchers.length; i++) { |
||||
if (mDispatchers[i] != null) { |
||||
mDispatchers[i].quit(); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Gets a sequence number. |
||||
*/ |
||||
public int getSequenceNumber() { |
||||
return mSequenceGenerator.incrementAndGet(); |
||||
} |
||||
|
||||
/** |
||||
* Gets the {@link Cache} instance being used. |
||||
*/ |
||||
public Cache getCache() { |
||||
return mCache; |
||||
} |
||||
|
||||
/** |
||||
* A simple predicate or filter interface for Requests, for use by |
||||
* {@link RequestQueue#cancelAll(RequestFilter)}. |
||||
*/ |
||||
public interface RequestFilter { |
||||
public boolean apply(Request<?> request); |
||||
} |
||||
|
||||
/** |
||||
* Cancels all requests in this queue for which the given filter applies. |
||||
* @param filter The filtering function to use |
||||
*/ |
||||
public void cancelAll(RequestFilter filter) { |
||||
synchronized (mCurrentRequests) { |
||||
for (Request<?> request : mCurrentRequests) { |
||||
if (filter.apply(request)) { |
||||
request.cancel(); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Cancels all requests in this queue with the given tag. Tag must be non-null |
||||
* and equality is by identity. |
||||
*/ |
||||
public void cancelAll(final Object tag) { |
||||
if (tag == null) { |
||||
throw new IllegalArgumentException("Cannot cancelAll with a null tag"); |
||||
} |
||||
cancelAll(new RequestFilter() { |
||||
@Override |
||||
public boolean apply(Request<?> request) { |
||||
return request.getTag() == tag; |
||||
} |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* Adds a Request to the dispatch queue. |
||||
* @param request The request to service |
||||
* @return The passed-in request |
||||
*/ |
||||
public <T> Request<T> add(Request<T> request) { |
||||
// Tag the request as belonging to this queue and add it to the set of current requests.
|
||||
request.setRequestQueue(this); |
||||
synchronized (mCurrentRequests) { |
||||
mCurrentRequests.add(request); |
||||
} |
||||
|
||||
// Process requests in the order they are added.
|
||||
request.setSequence(getSequenceNumber()); |
||||
request.addMarker("add-to-queue"); |
||||
|
||||
// If the request is uncacheable, skip the cache queue and go straight to the network.
|
||||
if (!request.shouldCache()) { |
||||
mNetworkQueue.add(request); |
||||
return request; |
||||
} |
||||
|
||||
// Insert request into stage if there's already a request with the same cache key in flight.
|
||||
synchronized (mWaitingRequests) { |
||||
String cacheKey = request.getCacheKey(); |
||||
if (mWaitingRequests.containsKey(cacheKey)) { |
||||
// There is already a request in flight. Queue up.
|
||||
Queue<Request<?>> stagedRequests = mWaitingRequests.get(cacheKey); |
||||
if (stagedRequests == null) { |
||||
stagedRequests = new LinkedList<Request<?>>(); |
||||
} |
||||
stagedRequests.add(request); |
||||
mWaitingRequests.put(cacheKey, stagedRequests); |
||||
if (VolleyLog.DEBUG) { |
||||
VolleyLog.v("Request for cacheKey=%s is in flight, putting on hold.", cacheKey); |
||||
} |
||||
} else { |
||||
// Insert 'null' queue for this cacheKey, indicating there is now a request in
|
||||
// flight.
|
||||
mWaitingRequests.put(cacheKey, null); |
||||
mCacheQueue.add(request); |
||||
} |
||||
return request; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Called from {@link Request#finish(String)}, indicating that processing of the given request |
||||
* has finished. |
||||
* |
||||
* <p>Releases waiting requests for <code>request.getCacheKey()</code> if |
||||
* <code>request.shouldCache()</code>.</p> |
||||
*/ |
||||
void finish(Request<?> request) { |
||||
// Remove from the set of requests currently being processed.
|
||||
synchronized (mCurrentRequests) { |
||||
mCurrentRequests.remove(request); |
||||
} |
||||
|
||||
if (request.shouldCache()) { |
||||
synchronized (mWaitingRequests) { |
||||
String cacheKey = request.getCacheKey(); |
||||
Queue<Request<?>> waitingRequests = mWaitingRequests.remove(cacheKey); |
||||
if (waitingRequests != null) { |
||||
if (VolleyLog.DEBUG) { |
||||
VolleyLog.v("Releasing %d waiting requests for cacheKey=%s.", |
||||
waitingRequests.size(), cacheKey); |
||||
} |
||||
// Process all queued up requests. They won't be considered as in flight, but
|
||||
// that's not a problem as the cache has been primed by 'request'.
|
||||
mCacheQueue.addAll(waitingRequests); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
@ -1,85 +0,0 @@ |
||||
/* |
||||
* Copyright (C) 2011 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.android.volley; |
||||
|
||||
/** |
||||
* Encapsulates a parsed response for delivery. |
||||
* |
||||
* @param <T> Parsed type of this response |
||||
*/ |
||||
public class Response<T> { |
||||
|
||||
/** Callback interface for delivering parsed responses. */ |
||||
public interface Listener<T> { |
||||
/** Called when a response is received. */ |
||||
public void onResponse(T response); |
||||
} |
||||
|
||||
/** Callback interface for delivering error responses. */ |
||||
public interface ErrorListener { |
||||
/** |
||||
* Callback method that an error has been occurred with the |
||||
* provided error code and optional user-readable message. |
||||
*/ |
||||
public void onErrorResponse(VolleyError error); |
||||
} |
||||
|
||||
/** Returns a successful response containing the parsed result. */ |
||||
public static <T> Response<T> success(T result, Cache.Entry cacheEntry) { |
||||
return new Response<T>(result, cacheEntry); |
||||
} |
||||
|
||||
/** |
||||
* Returns a failed response containing the given error code and an optional |
||||
* localized message displayed to the user. |
||||
*/ |
||||
public static <T> Response<T> error(VolleyError error) { |
||||
return new Response<T>(error); |
||||
} |
||||
|
||||
/** Parsed response, or null in the case of error. */ |
||||
public final T result; |
||||
|
||||
/** Cache metadata for this response, or null in the case of error. */ |
||||
public final Cache.Entry cacheEntry; |
||||
|
||||
/** Detailed error information if <code>errorCode != OK</code>. */ |
||||
public final VolleyError error; |
||||
|
||||
/** True if this response was a soft-expired one and a second one MAY be coming. */ |
||||
public boolean intermediate = false; |
||||
|
||||
/** |
||||
* Returns whether this response is considered successful. |
||||
*/ |
||||
public boolean isSuccess() { |
||||
return error == null; |
||||
} |
||||
|
||||
|
||||
private Response(T result, Cache.Entry cacheEntry) { |
||||
this.result = result; |
||||
this.cacheEntry = cacheEntry; |
||||
this.error = null; |
||||
} |
||||
|
||||
private Response(VolleyError error) { |
||||
this.result = null; |
||||
this.cacheEntry = null; |
||||
this.error = error; |
||||
} |
||||
} |
@ -1,35 +0,0 @@ |
||||
/* |
||||
* Copyright (C) 2011 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.android.volley; |
||||
|
||||
public interface ResponseDelivery { |
||||
/** |
||||
* Parses a response from the network or cache and delivers it. |
||||
*/ |
||||
public void postResponse(Request<?> request, Response<?> response); |
||||
|
||||
/** |
||||
* Parses a response from the network or cache and delivers it. The provided |
||||
* Runnable will be executed after delivery. |
||||
*/ |
||||
public void postResponse(Request<?> request, Response<?> response, Runnable runnable); |
||||
|
||||
/** |
||||
* Posts an error for the given request. |
||||
*/ |
||||
public void postError(Request<?> request, VolleyError error); |
||||
} |
@ -1,41 +0,0 @@ |
||||
/* |
||||
* Copyright (C) 2011 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.android.volley; |
||||
|
||||
/** |
||||
* Retry policy for a request. |
||||
*/ |
||||
public interface RetryPolicy { |
||||
|
||||
/** |
||||
* Returns the current timeout (used for logging). |
||||
*/ |
||||
public int getCurrentTimeout(); |
||||
|
||||
/** |
||||
* Returns the current retry count (used for logging). |
||||
*/ |
||||
public int getCurrentRetryCount(); |
||||
|
||||
/** |
||||
* Prepares for the next retry by applying a backoff to the timeout. |
||||
* @param error The error code of the last attempt. |
||||
* @throws VolleyError In the event that the retry could not be performed (for example if we |
||||
* ran out of attempts), the passed in error is thrown. |
||||
*/ |
||||
public void retry(VolleyError error) throws VolleyError; |
||||
} |
@ -1,34 +0,0 @@ |
||||
/* |
||||
* Copyright (C) 2011 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.android.volley; |
||||
|
||||
import com.android.volley.NetworkResponse; |
||||
import com.android.volley.VolleyError; |
||||
|
||||
/** |
||||
* Indicates that the error responded with an error response. |
||||
*/ |
||||
@SuppressWarnings("serial") |
||||
public class ServerError extends VolleyError { |
||||
public ServerError(NetworkResponse networkResponse) { |
||||
super(networkResponse); |
||||
} |
||||
|
||||
public ServerError() { |
||||
super(); |
||||
} |
||||
} |
@ -1,23 +0,0 @@ |
||||
/* |
||||
* Copyright (C) 2011 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.android.volley; |
||||
|
||||
/** |
||||
* Indicates that the connection or the socket timed out. |
||||
*/ |
||||
@SuppressWarnings("serial") |
||||
public class TimeoutError extends VolleyError { } |
@ -1,48 +0,0 @@ |
||||
/* |
||||
* Copyright (C) 2011 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.android.volley; |
||||
|
||||
/** |
||||
* Exception style class encapsulating Volley errors |
||||
*/ |
||||
@SuppressWarnings("serial") |
||||
public class VolleyError extends Exception { |
||||
public final NetworkResponse networkResponse; |
||||
|
||||
public VolleyError() { |
||||
networkResponse = null; |
||||
} |
||||
|
||||
public VolleyError(NetworkResponse response) { |
||||
networkResponse = response; |
||||
} |
||||
|
||||
public VolleyError(String exceptionMessage) { |
||||
super(exceptionMessage); |
||||
networkResponse = null; |
||||
} |
||||
|
||||
public VolleyError(String exceptionMessage, Throwable reason) { |
||||
super(exceptionMessage, reason); |
||||
networkResponse = null; |
||||
} |
||||
|
||||
public VolleyError(Throwable cause) { |
||||
super(cause); |
||||
networkResponse = null; |
||||
} |
||||
} |
@ -1,179 +0,0 @@ |
||||
/* |
||||
* Copyright (C) 2011 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.android.volley; |
||||
|
||||
import android.os.SystemClock; |
||||
import android.util.Log; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.List; |
||||
import java.util.Locale; |
||||
|
||||
/** Logging helper class. */ |
||||
public class VolleyLog { |
||||
public static String TAG = "Volley"; |
||||
|
||||
public static boolean DEBUG = Log.isLoggable(TAG, Log.VERBOSE); |
||||
|
||||
/** |
||||
* Customize the log tag for your application, so that other apps |
||||
* using Volley don't mix their logs with yours. |
||||
* <br /> |
||||
* Enable the log property for your tag before starting your app: |
||||
* <br /> |
||||
* {@code adb shell setprop log.tag.<tag>} |
||||
*/ |
||||
public static void setTag(String tag) { |
||||
d("Changing log tag to %s", tag); |
||||
TAG = tag; |
||||
|
||||
// Reinitialize the DEBUG "constant"
|
||||
DEBUG = Log.isLoggable(TAG, Log.VERBOSE); |
||||
} |
||||
|
||||
public static void v(String format, Object... args) { |
||||
if (DEBUG) { |
||||
Log.v(TAG, buildMessage(format, args)); |
||||
} |
||||
} |
||||
|
||||
public static void d(String format, Object... args) { |
||||
if (DEBUG) { |
||||
Log.d(TAG, buildMessage(format, args)); |
||||
} |
||||
} |
||||
|
||||
public static void e(String format, Object... args) { |
||||
Log.e(TAG, buildMessage(format, args)); |
||||
} |
||||
|
||||
public static void e(Throwable tr, String format, Object... args) { |
||||
Log.e(TAG, buildMessage(format, args), tr); |
||||
} |
||||
|
||||
public static void wtf(String format, Object... args) { |
||||
Log.wtf(TAG, buildMessage(format, args)); |
||||
} |
||||
|
||||
public static void wtf(Throwable tr, String format, Object... args) { |
||||
Log.wtf(TAG, buildMessage(format, args), tr); |
||||
} |
||||
|
||||
/** |
||||
* Formats the caller's provided message and prepends useful info like |
||||
* calling thread ID and method name. |
||||
*/ |
||||
private static String buildMessage(String format, Object... args) { |
||||
String msg = (args == null) ? format : String.format(Locale.US, format, args); |
||||
StackTraceElement[] trace = new Throwable().fillInStackTrace().getStackTrace(); |
||||
|
||||
String caller = "<unknown>"; |
||||
// Walk up the stack looking for the first caller outside of VolleyLog.
|
||||
// It will be at least two frames up, so start there.
|
||||
for (int i = 2; i < trace.length; i++) { |
||||
Class<?> clazz = trace[i].getClass(); |
||||
if (!clazz.equals(VolleyLog.class)) { |
||||
String callingClass = trace[i].getClassName(); |
||||
callingClass = callingClass.substring(callingClass.lastIndexOf('.') + 1); |
||||
callingClass = callingClass.substring(callingClass.lastIndexOf('$') + 1); |
||||
|
||||
caller = callingClass + "." + trace[i].getMethodName(); |
||||
break; |
||||
} |
||||
} |
||||
return String.format(Locale.US, "[%d] %s: %s", |
||||
Thread.currentThread().getId(), caller, msg); |
||||
} |
||||
|
||||
/** |
||||
* A simple event log with records containing a name, thread ID, and timestamp. |
||||
*/ |
||||
static class MarkerLog { |
||||
public static final boolean ENABLED = VolleyLog.DEBUG; |
||||
|
||||
/** Minimum duration from first marker to last in an marker log to warrant logging. */ |
||||
private static final long MIN_DURATION_FOR_LOGGING_MS = 0; |
||||
|
||||
private static class Marker { |
||||
public final String name; |
||||
public final long thread; |
||||
public final long time; |
||||
|
||||
public Marker(String name, long thread, long time) { |
||||
this.name = name; |
||||
this.thread = thread; |
||||
this.time = time; |
||||
} |
||||
} |
||||
|
||||
private final List<Marker> mMarkers = new ArrayList<Marker>(); |
||||
private boolean mFinished = false; |
||||
|
||||
/** Adds a marker to this log with the specified name. */ |
||||
public synchronized void add(String name, long threadId) { |
||||
if (mFinished) { |
||||
throw new IllegalStateException("Marker added to finished log"); |
||||
} |
||||
|
||||
mMarkers.add(new Marker(name, threadId, SystemClock.elapsedRealtime())); |
||||
} |
||||
|
||||
/** |
||||
* Closes the log, dumping it to logcat if the time difference between |
||||
* the first and last markers is greater than {@link #MIN_DURATION_FOR_LOGGING_MS}. |
||||
* @param header Header string to print above the marker log. |
||||
*/ |
||||
public synchronized void finish(String header) { |
||||
mFinished = true; |
||||
|
||||
long duration = getTotalDuration(); |
||||
if (duration <= MIN_DURATION_FOR_LOGGING_MS) { |
||||
return; |
||||
} |
||||
|
||||
long prevTime = mMarkers.get(0).time; |
||||
d("(%-4d ms) %s", duration, header); |
||||
for (Marker marker : mMarkers) { |
||||
long thisTime = marker.time; |
||||
d("(+%-4d) [%2d] %s", (thisTime - prevTime), marker.thread, marker.name); |
||||
prevTime = thisTime; |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
protected void finalize() throws Throwable { |
||||
// Catch requests that have been collected (and hence end-of-lifed)
|
||||
// but had no debugging output printed for them.
|
||||
if (!mFinished) { |
||||
finish("Request on the loose"); |
||||
e("Marker log finalized without finish() - uncaught exit point for request"); |
||||
} |
||||
super.finalize(); |
||||
} |
||||
|
||||
/** Returns the time difference between the first and last events in this log. */ |
||||
private long getTotalDuration() { |
||||
if (mMarkers.size() == 0) { |
||||
return 0; |
||||
} |
||||
|
||||
long first = mMarkers.get(0).time; |
||||
long last = mMarkers.get(mMarkers.size() - 1).time; |
||||
return last - first; |
||||
} |
||||
} |
||||
} |
@ -1,331 +0,0 @@ |
||||
package com.android.volley.compat; |
||||
|
||||
import java.io.IOException; |
||||
import java.io.InputStream; |
||||
import java.io.OutputStream; |
||||
import java.net.InetAddress; |
||||
import java.net.SocketAddress; |
||||
import java.net.SocketException; |
||||
import java.nio.channels.SocketChannel; |
||||
|
||||
import javax.net.ssl.HandshakeCompletedListener; |
||||
import javax.net.ssl.SSLParameters; |
||||
import javax.net.ssl.SSLSession; |
||||
import javax.net.ssl.SSLSocket; |
||||
|
||||
/** |
||||
* Created by robUx4 on 25/10/2014. |
||||
*/ |
||||
public class DelegateSSLSocket extends SSLSocket { |
||||
|
||||
protected final SSLSocket delegate; |
||||
|
||||
DelegateSSLSocket(SSLSocket delegate) { |
||||
this.delegate = delegate; |
||||
} |
||||
|
||||
@Override |
||||
public String[] getSupportedCipherSuites() { |
||||
return delegate.getSupportedCipherSuites(); |
||||
} |
||||
|
||||
@Override |
||||
public String[] getEnabledCipherSuites() { |
||||
return delegate.getEnabledCipherSuites(); |
||||
} |
||||
|
||||
@Override |
||||
public void setEnabledCipherSuites(String[] suites) { |
||||
delegate.setEnabledCipherSuites(suites); |
||||
} |
||||
|
||||
@Override |
||||
public String[] getSupportedProtocols() { |
||||
return delegate.getSupportedProtocols(); |
||||
} |
||||
|
||||
@Override |
||||
public String[] getEnabledProtocols() { |
||||
return delegate.getEnabledProtocols(); |
||||
} |
||||
|
||||
@Override |
||||
public void setEnabledProtocols(String[] protocols) { |
||||
delegate.setEnabledProtocols(protocols); |
||||
} |
||||
|
||||
@Override |
||||
public SSLSession getSession() { |
||||
return delegate.getSession(); |
||||
} |
||||
|
||||
@Override |
||||
public void addHandshakeCompletedListener(HandshakeCompletedListener listener) { |
||||
delegate.addHandshakeCompletedListener(listener); |
||||
} |
||||
|
||||
@Override |
||||
public void removeHandshakeCompletedListener(HandshakeCompletedListener listener) { |
||||
delegate.removeHandshakeCompletedListener(listener); |
||||
} |
||||
|
||||
@Override |
||||
public void startHandshake() throws IOException { |
||||
delegate.startHandshake(); |
||||
} |
||||
|
||||
@Override |
||||
public void setUseClientMode(boolean mode) { |
||||
delegate.setUseClientMode(mode); |
||||
} |
||||
|
||||
@Override |
||||
public boolean getUseClientMode() { |
||||
return delegate.getUseClientMode(); |
||||
} |
||||
|
||||
@Override |
||||
public void setNeedClientAuth(boolean need) { |
||||
delegate.setNeedClientAuth(need); |
||||
} |
||||
|
||||
@Override |
||||
public void setWantClientAuth(boolean want) { |
||||
delegate.setWantClientAuth(want); |
||||
} |
||||
|
||||
@Override |
||||
public boolean getNeedClientAuth() { |
||||
return delegate.getNeedClientAuth(); |
||||
} |
||||
|
||||
@Override |
||||
public boolean getWantClientAuth() { |
||||
return delegate.getWantClientAuth(); |
||||
} |
||||
|
||||
@Override |
||||
public void setEnableSessionCreation(boolean flag) { |
||||
delegate.setEnableSessionCreation(flag); |
||||
} |
||||
|
||||
@Override |
||||
public boolean getEnableSessionCreation() { |
||||
return delegate.getEnableSessionCreation(); |
||||
} |
||||
|
||||
@Override |
||||
public void bind(SocketAddress localAddr) throws IOException { |
||||
delegate.bind(localAddr); |
||||
} |
||||
|
||||
@Override |
||||
public synchronized void close() throws IOException { |
||||
delegate.close(); |
||||
} |
||||
|
||||
@Override |
||||
public void connect(SocketAddress remoteAddr) throws IOException { |
||||
delegate.connect(remoteAddr); |
||||
} |
||||
|
||||
@Override |
||||
public void connect(SocketAddress remoteAddr, int timeout) throws IOException { |
||||
delegate.connect(remoteAddr, timeout); |
||||
} |
||||
|
||||
@Override |
||||
public SocketChannel getChannel() { |
||||
return delegate.getChannel(); |
||||
} |
||||
|
||||
@Override |
||||
public InetAddress getInetAddress() { |
||||
return delegate.getInetAddress(); |
||||
} |
||||
|
||||
@Override |
||||
public InputStream getInputStream() throws IOException { |
||||
return delegate.getInputStream(); |
||||
} |
||||
|
||||
@Override |
||||
public boolean getKeepAlive() throws SocketException { |
||||
return delegate.getKeepAlive(); |
||||
} |
||||
|
||||
@Override |
||||
public InetAddress getLocalAddress() { |
||||
return delegate.getLocalAddress(); |
||||
} |
||||
|
||||
@Override |
||||
public int getLocalPort() { |
||||
return delegate.getLocalPort(); |
||||
} |
||||
|
||||
@Override |
||||
public SocketAddress getLocalSocketAddress() { |
||||
return delegate.getLocalSocketAddress(); |
||||
} |
||||
|
||||
@Override |
||||
public boolean getOOBInline() throws SocketException { |
||||
return delegate.getOOBInline(); |
||||
} |
||||
|
||||
@Override |
||||
public OutputStream getOutputStream() throws IOException { |
||||
return delegate.getOutputStream(); |
||||
} |
||||
|
||||
@Override |
||||
public int getPort() { |
||||
return delegate.getPort(); |
||||
} |
||||
|
||||
@Override |
||||
public synchronized int getReceiveBufferSize() throws SocketException { |
||||
return delegate.getReceiveBufferSize(); |
||||
} |
||||
|
||||
@Override |
||||
public SocketAddress getRemoteSocketAddress() { |
||||
return delegate.getRemoteSocketAddress(); |
||||
} |
||||
|
||||
@Override |
||||
public boolean getReuseAddress() throws SocketException { |
||||
return delegate.getReuseAddress(); |
||||
} |
||||
|
||||
@Override |
||||
public synchronized int getSendBufferSize() throws SocketException { |
||||
return delegate.getSendBufferSize(); |
||||
} |
||||
|
||||
@Override |
||||
public int getSoLinger() throws SocketException { |
||||
return delegate.getSoLinger(); |
||||
} |
||||
|
||||
@Override |
||||
public synchronized int getSoTimeout() throws SocketException { |
||||
return delegate.getSoTimeout(); |
||||
} |
||||
|
||||
@Override |
||||
public boolean getTcpNoDelay() throws SocketException { |
||||
return delegate.getTcpNoDelay(); |
||||
} |
||||
|
||||
@Override |
||||
public int getTrafficClass() throws SocketException { |
||||
return delegate.getTrafficClass(); |
||||
} |
||||
|
||||
@Override |
||||
public boolean isBound() { |
||||
return delegate.isBound(); |
||||
} |
||||
|
||||
@Override |
||||
public boolean isClosed() { |
||||
return delegate.isClosed(); |
||||
} |
||||
|
||||
@Override |
||||
public boolean isConnected() { |
||||
return delegate.isConnected(); |
||||
} |
||||
|
||||
@Override |
||||
public boolean isInputShutdown() { |
||||
return delegate.isInputShutdown(); |
||||
} |
||||
|
||||
@Override |
||||
public boolean isOutputShutdown() { |
||||
return delegate.isOutputShutdown(); |
||||
} |
||||
|
||||
@Override |
||||
public void sendUrgentData(int value) throws IOException { |
||||
delegate.sendUrgentData(value); |
||||
} |
||||
|
||||
@Override |
||||
public void setKeepAlive(boolean keepAlive) throws SocketException { |
||||
delegate.setKeepAlive(keepAlive); |
||||
} |
||||
|
||||
@Override |
||||
public void setOOBInline(boolean oobinline) throws SocketException { |
||||
delegate.setOOBInline(oobinline); |
||||
} |
||||
|
||||
@Override |
||||
public void setPerformancePreferences(int connectionTime, int latency, int bandwidth) { |
||||
delegate.setPerformancePreferences(connectionTime, latency, bandwidth); |
||||
} |
||||
|
||||
@Override |
||||
public synchronized void setReceiveBufferSize(int size) throws SocketException { |
||||
delegate.setReceiveBufferSize(size); |
||||
} |
||||
|
||||
@Override |
||||
public void setReuseAddress(boolean reuse) throws SocketException { |
||||
delegate.setReuseAddress(reuse); |
||||
} |
||||
|
||||
@Override |
||||
public synchronized void setSendBufferSize(int size) throws SocketException { |
||||
delegate.setSendBufferSize(size); |
||||
} |
||||
|
||||
@Override |
||||
public void setSoLinger(boolean on, int timeout) throws SocketException { |
||||
delegate.setSoLinger(on, timeout); |
||||
} |
||||
|
||||
@Override |
||||
public synchronized void setSoTimeout(int timeout) throws SocketException { |
||||
delegate.setSoTimeout(timeout); |
||||
} |
||||
|
||||
@Override |
||||
public void setSSLParameters(SSLParameters p) { |
||||
delegate.setSSLParameters(p); |
||||
} |
||||
|
||||
@Override |
||||
public void setTcpNoDelay(boolean on) throws SocketException { |
||||
delegate.setTcpNoDelay(on); |
||||
} |
||||
|
||||
@Override |
||||
public void setTrafficClass(int value) throws SocketException { |
||||
delegate.setTrafficClass(value); |
||||
} |
||||
|
||||
@Override |
||||
public void shutdownInput() throws IOException { |
||||
delegate.shutdownInput(); |
||||
} |
||||
|
||||
@Override |
||||
public void shutdownOutput() throws IOException { |
||||
delegate.shutdownOutput(); |
||||
} |
||||
|
||||
@Override |
||||
public String toString() { |
||||
return delegate.toString(); |
||||
} |
||||
|
||||
@Override |
||||
public boolean equals(Object o) { |
||||
return delegate.equals(o); |
||||
} |
||||
} |
@ -1,113 +0,0 @@ |
||||
package com.android.volley.compat; |
||||
|
||||
import com.android.volley.VolleyLog; |
||||
|
||||
import java.io.IOException; |
||||
import java.net.InetAddress; |
||||
import java.net.Socket; |
||||
import java.util.ArrayList; |
||||
import java.util.Arrays; |
||||
import java.util.List; |
||||
|
||||
import javax.net.ssl.HttpsURLConnection; |
||||
import javax.net.ssl.SSLSocket; |
||||
import javax.net.ssl.SSLSocketFactory; |
||||
|
||||
public class NoSSLv3Compat { |
||||
/** |
||||
* An {@link javax.net.ssl.SSLSocket} that doesn't allow {@code SSLv3} only connections |
||||
* <p>fixes https://github.com/koush/ion/issues/386</p>
|
||||
* <p>Android bug report: https://code.google.com/p/android/issues/detail?id=78187</p>
|
||||
*/ |
||||
private static class NoSSLv3SSLSocket extends DelegateSSLSocket { |
||||
|
||||
private NoSSLv3SSLSocket(SSLSocket delegate) { |
||||
super(delegate); |
||||
|
||||
/*String canonicalName = delegate.getClass().getCanonicalName(); |
||||
if (!canonicalName.equals("org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl")) { |
||||
// try replicate the code from HttpConnection.setupSecureSocket()
|
||||
try { |
||||
Method msetUseSessionTickets = delegate.getClass().getMethod("setUseSessionTickets", boolean.class); |
||||
if (null != msetUseSessionTickets) { |
||||
msetUseSessionTickets.invoke(delegate, true); |
||||
} |
||||
} catch (NoSuchMethodException ignored) { |
||||
} catch (InvocationTargetException ignored) { |
||||
} catch (IllegalAccessException ignored) { |
||||
} |
||||
}*/ |
||||
} |
||||
|
||||
@Override |
||||
public void setEnabledProtocols(String[] protocols) { |
||||
if (protocols != null && protocols.length == 1 && "SSLv3".equals(protocols[0])) { |
||||
// no way jose
|
||||
// see issue https://code.google.com/p/android/issues/detail?id=78187
|
||||
List<String> enabledProtocols = new ArrayList<>(Arrays.asList(delegate.getEnabledProtocols())); |
||||
if (enabledProtocols.size() > 1) { |
||||
enabledProtocols.remove("SSLv3"); |
||||
VolleyLog.d("Removed SSLv3 from enabled protocols"); |
||||
} else { |
||||
VolleyLog.d("SSL stuck with protocol available for " + String.valueOf(enabledProtocols)); |
||||
} |
||||
protocols = enabledProtocols.toArray(new String[enabledProtocols.size()]); |
||||
} |
||||
|
||||
super.setEnabledProtocols(protocols); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* {@link javax.net.ssl.SSLSocketFactory} that doesn't allow {@code SSLv3} only connections |
||||
*/ |
||||
public static class NoSSLv3Factory extends SSLSocketFactory { |
||||
private final SSLSocketFactory delegate; |
||||
|
||||
public NoSSLv3Factory() { |
||||
this.delegate = HttpsURLConnection.getDefaultSSLSocketFactory(); |
||||
} |
||||
|
||||
@Override |
||||
public String[] getDefaultCipherSuites() { |
||||
return delegate.getDefaultCipherSuites(); |
||||
} |
||||
|
||||
@Override |
||||
public String[] getSupportedCipherSuites() { |
||||
return delegate.getSupportedCipherSuites(); |
||||
} |
||||
|
||||
private static Socket makeSocketSafe(Socket socket) { |
||||
if (socket instanceof SSLSocket) { |
||||
socket = new NoSSLv3SSLSocket((SSLSocket) socket); |
||||
} |
||||
return socket; |
||||
} |
||||
|
||||
@Override |
||||
public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException { |
||||
return makeSocketSafe(delegate.createSocket(s, host, port, autoClose)); |
||||
} |
||||
|
||||
@Override |
||||
public Socket createSocket(String host, int port) throws IOException { |
||||
return makeSocketSafe(delegate.createSocket(host, port)); |
||||
} |
||||
|
||||
@Override |
||||
public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException { |
||||
return makeSocketSafe(delegate.createSocket(host, port, localHost, localPort)); |
||||
} |
||||
|
||||
@Override |
||||
public Socket createSocket(InetAddress host, int port) throws IOException { |
||||
return makeSocketSafe(delegate.createSocket(host, port)); |
||||
} |
||||
|
||||
@Override |
||||
public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException { |
||||
return makeSocketSafe(delegate.createSocket(address, port, localAddress, localPort)); |
||||
} |
||||
} |
||||
} |
@ -1,102 +0,0 @@ |
||||
/* |
||||
* Copyright (C) 2011 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.android.volley.toolbox; |
||||
|
||||
import com.android.volley.AuthFailureError; |
||||
|
||||
import android.accounts.Account; |
||||
import android.accounts.AccountManager; |
||||
import android.accounts.AccountManagerFuture; |
||||
import android.content.Context; |
||||
import android.content.Intent; |
||||
import android.os.Bundle; |
||||
|
||||
/** |
||||
* An Authenticator that uses {@link AccountManager} to get auth |
||||
* tokens of a specified type for a specified account. |
||||
*/ |
||||
public class AndroidAuthenticator implements Authenticator { |
||||
private final Context mContext; |
||||
private final Account mAccount; |
||||
private final String mAuthTokenType; |
||||
private final boolean mNotifyAuthFailure; |
||||
|
||||
/** |
||||
* Creates a new authenticator. |
||||
* @param context Context for accessing AccountManager |
||||
* @param account Account to authenticate as |
||||
* @param authTokenType Auth token type passed to AccountManager |
||||
*/ |
||||
public AndroidAuthenticator(Context context, Account account, String authTokenType) { |
||||
this(context, account, authTokenType, false); |
||||
} |
||||
|
||||
/** |
||||
* Creates a new authenticator. |
||||
* @param context Context for accessing AccountManager |
||||
* @param account Account to authenticate as |
||||
* @param authTokenType Auth token type passed to AccountManager |
||||
* @param notifyAuthFailure Whether to raise a notification upon auth failure |
||||
*/ |
||||
public AndroidAuthenticator(Context context, Account account, String authTokenType, |
||||
boolean notifyAuthFailure) { |
||||
mContext = context; |
||||
mAccount = account; |
||||
mAuthTokenType = authTokenType; |
||||
mNotifyAuthFailure = notifyAuthFailure; |
||||
} |
||||
|
||||
/** |
||||
* Returns the Account being used by this authenticator. |
||||
*/ |
||||
public Account getAccount() { |
||||
return mAccount; |
||||
} |
||||
|
||||
// TODO: Figure out what to do about notifyAuthFailure
|
||||
@SuppressWarnings("deprecation") |
||||
@Override |
||||
public String getAuthToken() throws AuthFailureError { |
||||
final AccountManager accountManager = AccountManager.get(mContext); |
||||
AccountManagerFuture<Bundle> future = accountManager.getAuthToken(mAccount, |
||||
mAuthTokenType, mNotifyAuthFailure, null, null); |
||||
Bundle result; |
||||
try { |
||||
result = future.getResult(); |
||||
} catch (Exception e) { |
||||
throw new AuthFailureError("Error while retrieving auth token", e); |
||||
} |
||||
String authToken = null; |
||||
if (future.isDone() && !future.isCancelled()) { |
||||
if (result.containsKey(AccountManager.KEY_INTENT)) { |
||||
Intent intent = result.getParcelable(AccountManager.KEY_INTENT); |
||||
throw new AuthFailureError(intent); |
||||
} |
||||
authToken = result.getString(AccountManager.KEY_AUTHTOKEN); |
||||
} |
||||
if (authToken == null) { |
||||
throw new AuthFailureError("Got null auth token for type: " + mAuthTokenType); |
||||
} |
||||
|
||||
return authToken; |
||||
} |
||||
|
||||
@Override |
||||
public void invalidateAuthToken(String authToken) { |
||||
AccountManager.get(mContext).invalidateAuthToken(mAccount.type, authToken); |
||||
} |
||||
} |
@ -1,36 +0,0 @@ |
||||
/* |
||||
* Copyright (C) 2011 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.android.volley.toolbox; |
||||
|
||||
import com.android.volley.AuthFailureError; |
||||
|
||||
/** |
||||
* An interface for interacting with auth tokens. |
||||
*/ |
||||
public interface Authenticator { |
||||
/** |
||||
* Synchronously retrieves an auth token. |
||||
* |
||||
* @throws AuthFailureError If authentication did not succeed |
||||
*/ |
||||
public String getAuthToken() throws AuthFailureError; |
||||
|
||||
/** |
||||
* Invalidates the provided auth token. |
||||
*/ |
||||
public void invalidateAuthToken(String authToken); |
||||
} |
@ -1,263 +0,0 @@ |
||||
/* |
||||
* Copyright (C) 2011 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.android.volley.toolbox; |
||||
|
||||
import android.os.SystemClock; |
||||
|
||||
import com.android.volley.AuthFailureError; |
||||
import com.android.volley.Cache; |
||||
import com.android.volley.Cache.Entry; |
||||
import com.android.volley.Network; |
||||
import com.android.volley.NetworkError; |
||||
import com.android.volley.NetworkResponse; |
||||
import com.android.volley.NoConnectionError; |
||||
import com.android.volley.Request; |
||||
import com.android.volley.RetryPolicy; |
||||
import com.android.volley.ServerError; |
||||
import com.android.volley.TimeoutError; |
||||
import com.android.volley.VolleyError; |
||||
import com.android.volley.VolleyLog; |
||||
|
||||
import org.apache.http.Header; |
||||
import org.apache.http.HttpEntity; |
||||
import org.apache.http.HttpResponse; |
||||
import org.apache.http.HttpStatus; |
||||
import org.apache.http.StatusLine; |
||||
import org.apache.http.conn.ConnectTimeoutException; |
||||
import org.apache.http.impl.cookie.DateUtils; |
||||
|
||||
import java.io.IOException; |
||||
import java.io.InputStream; |
||||
import java.net.MalformedURLException; |
||||
import java.net.SocketTimeoutException; |
||||
import java.util.Collections; |
||||
import java.util.Date; |
||||
import java.util.HashMap; |
||||
import java.util.Map; |
||||
import java.util.TreeMap; |
||||
|
||||
/** |
||||
* A network performing Volley requests over an {@link HttpStack}. |
||||
*/ |
||||
public class BasicNetwork implements Network { |
||||
protected static final boolean DEBUG = VolleyLog.DEBUG; |
||||
|
||||
private static int SLOW_REQUEST_THRESHOLD_MS = 3000; |
||||
|
||||
private static int DEFAULT_POOL_SIZE = 4096; |
||||
|
||||
protected final HttpStack mHttpStack; |
||||
|
||||
protected final ByteArrayPool mPool; |
||||
|
||||
/** |
||||
* @param httpStack HTTP stack to be used |
||||
*/ |
||||
public BasicNetwork(HttpStack httpStack) { |
||||
// If a pool isn't passed in, then build a small default pool that will give us a lot of
|
||||
// benefit and not use too much memory.
|
||||
this(httpStack, new ByteArrayPool(DEFAULT_POOL_SIZE)); |
||||
} |
||||
|
||||
/** |
||||
* @param httpStack HTTP stack to be used |
||||
* @param pool a buffer pool that improves GC performance in copy operations |
||||
*/ |
||||
public BasicNetwork(HttpStack httpStack, ByteArrayPool pool) { |
||||
mHttpStack = httpStack; |
||||
mPool = pool; |
||||
} |
||||
|
||||
@Override |
||||
public NetworkResponse performRequest(Request<?> request) throws VolleyError { |
||||
long requestStart = SystemClock.elapsedRealtime(); |
||||
while (true) { |
||||
HttpResponse httpResponse = null; |
||||
byte[] responseContents = null; |
||||
Map<String, String> responseHeaders = Collections.emptyMap(); |
||||
try { |
||||
// Gather headers.
|
||||
Map<String, String> headers = new HashMap<String, String>(); |
||||
addCacheHeaders(headers, request.getCacheEntry()); |
||||
httpResponse = mHttpStack.performRequest(request, headers); |
||||
StatusLine statusLine = httpResponse.getStatusLine(); |
||||
int statusCode = statusLine.getStatusCode(); |
||||
|
||||
responseHeaders = convertHeaders(httpResponse.getAllHeaders()); |
||||
// Handle cache validation.
|
||||
if (statusCode == HttpStatus.SC_NOT_MODIFIED) { |
||||
|
||||
Entry entry = request.getCacheEntry(); |
||||
if (entry == null) { |
||||
return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED, null, |
||||
responseHeaders, true); |
||||
} |
||||
|
||||
// A HTTP 304 response does not have all header fields. We
|
||||
// have to use the header fields from the cache entry plus
|
||||
// the new ones from the response.
|
||||
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
|
||||
entry.responseHeaders.putAll(responseHeaders); |
||||
return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED, entry.data, |
||||
entry.responseHeaders, true); |
||||
} |
||||
|
||||
// Some responses such as 204s do not have content. We must check.
|
||||
if (httpResponse.getEntity() != null) { |
||||
responseContents = entityToBytes(httpResponse.getEntity()); |
||||
} else { |
||||
// Add 0 byte response as a way of honestly representing a
|
||||
// no-content request.
|
||||
responseContents = new byte[0]; |
||||
} |
||||
|
||||
// if the request is slow, log it.
|
||||
long requestLifetime = SystemClock.elapsedRealtime() - requestStart; |
||||
logSlowRequests(requestLifetime, request, responseContents, statusLine); |
||||
|
||||
if (statusCode < 200 || statusCode > 299) { |
||||
throw new IOException(); |
||||
} |
||||
return new NetworkResponse(statusCode, responseContents, responseHeaders, false); |
||||
} catch (SocketTimeoutException e) { |
||||
attemptRetryOnException("socket", request, new TimeoutError()); |
||||
} catch (ConnectTimeoutException e) { |
||||
attemptRetryOnException("connection", request, new TimeoutError()); |
||||
} catch (MalformedURLException e) { |
||||
throw new RuntimeException("Bad URL " + request.getUrl(), e); |
||||
} catch (IOException e) { |
||||
int statusCode = 0; |
||||
NetworkResponse networkResponse = null; |
||||
if (httpResponse != null) { |
||||
statusCode = httpResponse.getStatusLine().getStatusCode(); |
||||
} else { |
||||
throw new NoConnectionError(e); |
||||
} |
||||
VolleyLog.e("Unexpected response code %d for %s", statusCode, request.getUrl()); |
||||
if (responseContents != null) { |
||||
networkResponse = new NetworkResponse(statusCode, responseContents, |
||||
responseHeaders, false); |
||||
if (statusCode == HttpStatus.SC_UNAUTHORIZED || |
||||
statusCode == HttpStatus.SC_FORBIDDEN) { |
||||
attemptRetryOnException("auth", |
||||
request, new AuthFailureError(networkResponse)); |
||||
} else { |
||||
// TODO: Only throw ServerError for 5xx status codes.
|
||||
throw new ServerError(networkResponse); |
||||
} |
||||
} else { |
||||
throw new NetworkError(networkResponse); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Logs requests that took over SLOW_REQUEST_THRESHOLD_MS to complete. |
||||
*/ |
||||
private void logSlowRequests(long requestLifetime, Request<?> request, |
||||
byte[] responseContents, StatusLine statusLine) { |
||||
if (DEBUG || requestLifetime > SLOW_REQUEST_THRESHOLD_MS) { |
||||
VolleyLog.d("HTTP response for request=<%s> [lifetime=%d], [size=%s], " + |
||||
"[rc=%d], [retryCount=%s]", request, requestLifetime, |
||||
responseContents != null ? responseContents.length : "null", |
||||
statusLine.getStatusCode(), request.getRetryPolicy().getCurrentRetryCount()); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Attempts to prepare the request for a retry. If there are no more attempts remaining in the |
||||
* request's retry policy, a timeout exception is thrown. |
||||
* @param request The request to use. |
||||
*/ |
||||
private static void attemptRetryOnException(String logPrefix, Request<?> request, |
||||
VolleyError exception) throws VolleyError { |
||||
RetryPolicy retryPolicy = request.getRetryPolicy(); |
||||
int oldTimeout = request.getTimeoutMs(); |
||||
|
||||
try { |
||||
retryPolicy.retry(exception); |
||||
} catch (VolleyError e) { |
||||
request.addMarker( |
||||
String.format("%s-timeout-giveup [timeout=%s]", logPrefix, oldTimeout)); |
||||
throw e; |
||||
} |
||||
request.addMarker(String.format("%s-retry [timeout=%s]", logPrefix, oldTimeout)); |
||||
} |
||||
|
||||
private void addCacheHeaders(Map<String, String> headers, Cache.Entry entry) { |
||||
// If there's no cache entry, we're done.
|
||||
if (entry == null) { |
||||
return; |
||||
} |
||||
|
||||
if (entry.etag != null) { |
||||
headers.put("If-None-Match", entry.etag); |
||||
} |
||||
|
||||
if (entry.serverDate > 0) { |
||||
Date refTime = new Date(entry.serverDate); |
||||
headers.put("If-Modified-Since", DateUtils.formatDate(refTime)); |
||||
} |
||||
} |
||||
|
||||
protected void logError(String what, String url, long start) { |
||||
long now = SystemClock.elapsedRealtime(); |
||||
VolleyLog.v("HTTP ERROR(%s) %d ms to fetch %s", what, (now - start), url); |
||||
} |
||||
|
||||
/** Reads the contents of HttpEntity into a byte[]. */ |
||||
private byte[] entityToBytes(HttpEntity entity) throws IOException, ServerError { |
||||
PoolingByteArrayOutputStream bytes = |
||||
new PoolingByteArrayOutputStream(mPool, (int) entity.getContentLength()); |
||||
byte[] buffer = null; |
||||
try { |
||||
InputStream in = entity.getContent(); |
||||
if (in == null) { |
||||
throw new ServerError(); |
||||
} |
||||
buffer = mPool.getBuf(1024); |
||||
int count; |
||||
while ((count = in.read(buffer)) != -1) { |
||||
bytes.write(buffer, 0, count); |
||||
} |
||||
return bytes.toByteArray(); |
||||
} finally { |
||||
try { |
||||
// Close the InputStream and release the resources by "consuming the content".
|
||||
entity.consumeContent(); |
||||
} catch (IOException e) { |
||||
// This can happen if there was an exception above that left the entity in
|
||||
// an invalid state.
|
||||
VolleyLog.v("Error occured when calling consumingContent"); |
||||
} |
||||
mPool.returnBuf(buffer); |
||||
bytes.close(); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Converts Headers[] to Map<String, String>. |
||||
*/ |
||||
protected static Map<String, String> convertHeaders(Header[] headers) { |
||||
Map<String, String> result = new TreeMap<String, String>(String.CASE_INSENSITIVE_ORDER); |
||||
for (int i = 0; i < headers.length; i++) { |
||||
result.put(headers[i].getName(), headers[i].getValue()); |
||||
} |
||||
return result; |
||||
} |
||||
} |
@ -1,135 +0,0 @@ |
||||
/* |
||||
* Copyright (C) 2012 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.android.volley.toolbox; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.Collections; |
||||
import java.util.Comparator; |
||||
import java.util.LinkedList; |
||||
import java.util.List; |
||||
|
||||
/** |
||||
* ByteArrayPool is a source and repository of <code>byte[]</code> objects. Its purpose is to |
||||
* supply those buffers to consumers who need to use them for a short period of time and then |
||||
* dispose of them. Simply creating and disposing such buffers in the conventional manner can |
||||
* considerable heap churn and garbage collection delays on Android, which lacks good management of |
||||
* short-lived heap objects. It may be advantageous to trade off some memory in the form of a |
||||
* permanently allocated pool of buffers in order to gain heap performance improvements; that is |
||||
* what this class does. |
||||
* <p> |
||||
* A good candidate user for this class is something like an I/O system that uses large temporary |
||||
* <code>byte[]</code> buffers to copy data around. In these use cases, often the consumer wants |
||||
* the buffer to be a certain minimum size to ensure good performance (e.g. when copying data chunks |
||||
* off of a stream), but doesn't mind if the buffer is larger than the minimum. Taking this into |
||||
* account and also to maximize the odds of being able to reuse a recycled buffer, this class is |
||||
* free to return buffers larger than the requested size. The caller needs to be able to gracefully |
||||
* deal with getting buffers any size over the minimum. |
||||
* <p> |
||||
* If there is not a suitably-sized buffer in its recycling pool when a buffer is requested, this |
||||
* class will allocate a new buffer and return it. |
||||
* <p> |
||||
* This class has no special ownership of buffers it creates; the caller is free to take a buffer |
||||
* it receives from this pool, use it permanently, and never return it to the pool; additionally, |
||||
* it is not harmful to return to this pool a buffer that was allocated elsewhere, provided there |
||||
* are no other lingering references to it. |
||||
* <p> |
||||
* This class ensures that the total size of the buffers in its recycling pool never exceeds a |
||||
* certain byte limit. When a buffer is returned that would cause the pool to exceed the limit, |
||||
* least-recently-used buffers are disposed. |
||||
*/ |
||||
public class ByteArrayPool { |
||||
/** The buffer pool, arranged both by last use and by buffer size */ |
||||
private List<byte[]> mBuffersByLastUse = new LinkedList<byte[]>(); |
||||
private List<byte[]> mBuffersBySize = new ArrayList<byte[]>(64); |
||||
|
||||
/** The total size of the buffers in the pool */ |
||||
private int mCurrentSize = 0; |
||||
|
||||
/** |
||||
* The maximum aggregate size of the buffers in the pool. Old buffers are discarded to stay |
||||
* under this limit. |
||||
*/ |
||||
private final int mSizeLimit; |
||||
|
||||
/** Compares buffers by size */ |
||||
protected static final Comparator<byte[]> BUF_COMPARATOR = new Comparator<byte[]>() { |
||||
@Override |
||||
public int compare(byte[] lhs, byte[] rhs) { |
||||
return lhs.length - rhs.length; |
||||
} |
||||
}; |
||||
|
||||
/** |
||||
* @param sizeLimit the maximum size of the pool, in bytes |
||||
*/ |
||||
public ByteArrayPool(int sizeLimit) { |
||||
mSizeLimit = sizeLimit; |
||||
} |
||||
|
||||
/** |
||||
* Returns a buffer from the pool if one is available in the requested size, or allocates a new |
||||
* one if a pooled one is not available. |
||||
* |
||||
* @param len the minimum size, in bytes, of the requested buffer. The returned buffer may be |
||||
* larger. |
||||
* @return a byte[] buffer is always returned. |
||||
*/ |
||||
public synchronized byte[] getBuf(int len) { |
||||
for (int i = 0; i < mBuffersBySize.size(); i++) { |
||||
byte[] buf = mBuffersBySize.get(i); |
||||
if (buf.length >= len) { |
||||
mCurrentSize -= buf.length; |
||||
mBuffersBySize.remove(i); |
||||
mBuffersByLastUse.remove(buf); |
||||
return buf; |
||||
} |
||||
} |
||||
return new byte[len]; |
||||
} |
||||
|
||||
/** |
||||
* Returns a buffer to the pool, throwing away old buffers if the pool would exceed its allotted |
||||
* size. |
||||
* |
||||
* @param buf the buffer to return to the pool. |
||||
*/ |
||||
public synchronized void returnBuf(byte[] buf) { |
||||
if (buf == null || buf.length > mSizeLimit) { |
||||
return; |
||||
} |
||||
mBuffersByLastUse.add(buf); |
||||
int pos = Collections.binarySearch(mBuffersBySize, buf, BUF_COMPARATOR); |
||||
if (pos < 0) { |
||||
pos = -pos - 1; |
||||
} |
||||
mBuffersBySize.add(pos, buf); |
||||
mCurrentSize += buf.length; |
||||
trim(); |
||||
} |
||||
|
||||
/** |
||||
* Removes buffers from the pool until it is under its size limit. |
||||
*/ |
||||
private synchronized void trim() { |
||||
while (mCurrentSize > mSizeLimit) { |
||||
byte[] buf = mBuffersByLastUse.remove(0); |
||||
mBuffersBySize.remove(buf); |
||||
mCurrentSize -= buf.length; |
||||
} |
||||
} |
||||
|
||||
} |
@ -1,70 +0,0 @@ |
||||
/* |
||||
* Copyright (C) 2011 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.android.volley.toolbox; |
||||
|
||||
import com.android.volley.Cache; |
||||
import com.android.volley.NetworkResponse; |
||||
import com.android.volley.Request; |
||||
import com.android.volley.Response; |
||||
|
||||
import android.os.Handler; |
||||
import android.os.Looper; |
||||
|
||||
/** |
||||
* A synthetic request used for clearing the cache. |
||||
*/ |
||||
public class ClearCacheRequest extends Request<Object> { |
||||
private final Cache mCache; |
||||
private final Runnable mCallback; |
||||
|
||||
/** |
||||
* Creates a synthetic request for clearing the cache. |
||||
* @param cache Cache to clear |
||||
* @param callback Callback to make on the main thread once the cache is clear, |
||||
* or null for none |
||||
*/ |
||||
public ClearCacheRequest(Cache cache, Runnable callback) { |
||||
super(Method.GET, null, null); |
||||
mCache = cache; |
||||
mCallback = callback; |
||||
} |
||||
|
||||
@Override |
||||
public boolean isCanceled() { |
||||
// This is a little bit of a hack, but hey, why not.
|
||||
mCache.clear(); |
||||
if (mCallback != null) { |
||||
Handler handler = new Handler(Looper.getMainLooper()); |
||||
handler.postAtFrontOfQueue(mCallback); |
||||
} |
||||
return true; |
||||
} |
||||
|
||||
@Override |
||||
public Priority getPriority() { |
||||
return Priority.IMMEDIATE; |
||||
} |
||||
|
||||
@Override |
||||
protected Response<Object> parseNetworkResponse(NetworkResponse response) { |
||||
return null; |
||||
} |
||||
|
||||
@Override |
||||
protected void deliverResponse(Object response) { |
||||
} |
||||
} |
@ -1,569 +0,0 @@ |
||||
/* |
||||
* Copyright (C) 2011 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.android.volley.toolbox; |
||||
|
||||
import android.os.SystemClock; |
||||
|
||||
import com.android.volley.Cache; |
||||
import com.android.volley.VolleyLog; |
||||
|
||||
import java.io.BufferedInputStream; |
||||
import java.io.BufferedOutputStream; |
||||
import java.io.EOFException; |
||||
import java.io.File; |
||||
import java.io.FileInputStream; |
||||
import java.io.FileOutputStream; |
||||
import java.io.FilterInputStream; |
||||
import java.io.IOException; |
||||
import java.io.InputStream; |
||||
import java.io.OutputStream; |
||||
import java.util.Collections; |
||||
import java.util.HashMap; |
||||
import java.util.Iterator; |
||||
import java.util.LinkedHashMap; |
||||
import java.util.Map; |
||||
|
||||
/** |
||||
* Cache implementation that caches files directly onto the hard disk in the specified |
||||
* directory. The default disk usage size is 5MB, but is configurable. |
||||
*/ |
||||
public class DiskBasedCache implements Cache { |
||||
|
||||
/** Map of the Key, CacheHeader pairs */ |
||||
private final Map<String, CacheHeader> mEntries = |
||||
new LinkedHashMap<String, CacheHeader>(16, .75f, true); |
||||
|
||||
/** Total amount of space currently used by the cache in bytes. */ |
||||
private long mTotalSize = 0; |
||||
|
||||
/** The root directory to use for the cache. */ |
||||
private final File mRootDirectory; |
||||
|
||||
/** The maximum size of the cache in bytes. */ |
||||
private final int mMaxCacheSizeInBytes; |
||||
|
||||
/** Default maximum disk usage in bytes. */ |
||||
private static final int DEFAULT_DISK_USAGE_BYTES = 5 * 1024 * 1024; |
||||
|
||||
/** High water mark percentage for the cache */ |
||||
private static final float HYSTERESIS_FACTOR = 0.9f; |
||||
|
||||
/** Magic number for current version of cache file format. */ |
||||
private static final int CACHE_MAGIC = 0x20140623; |
||||
|
||||
private static final int DEFAULT_DISK_FILES_MAX = 250; |
||||
|
||||
/** |
||||
* Constructs an instance of the DiskBasedCache at the specified directory. |
||||
* @param rootDirectory The root directory of the cache. |
||||
* @param maxCacheSizeInBytes The maximum size of the cache in bytes. |
||||
*/ |
||||
public DiskBasedCache(File rootDirectory, int maxCacheSizeInBytes) { |
||||
mRootDirectory = rootDirectory; |
||||
mMaxCacheSizeInBytes = maxCacheSizeInBytes; |
||||
} |
||||
|
||||
/** |
||||
* Constructs an instance of the DiskBasedCache at the specified directory using |
||||
* the default maximum cache size of 5MB. |
||||
* @param rootDirectory The root directory of the cache. |
||||
*/ |
||||
public DiskBasedCache(File rootDirectory) { |
||||
this(rootDirectory, DEFAULT_DISK_USAGE_BYTES); |
||||
} |
||||
|
||||
/** |
||||
* Clears the cache. Deletes all cached files from disk. |
||||
*/ |
||||
@Override |
||||
public synchronized void clear() { |
||||
File[] files = mRootDirectory.listFiles(); |
||||
if (files != null) { |
||||
for (File file : files) { |
||||
file.delete(); |
||||
} |
||||
} |
||||
mEntries.clear(); |
||||
mTotalSize = 0; |
||||
VolleyLog.d("Cache cleared."); |
||||
} |
||||
|
||||
/** |
||||
* Returns the cache entry with the specified key if it exists, null otherwise. |
||||
*/ |
||||
@Override |
||||
public synchronized Entry get(String key) { |
||||
CacheHeader entry = mEntries.get(key); |
||||
// if the entry does not exist, return.
|
||||
if (entry == null) { |
||||
return null; |
||||
} |
||||
|
||||
File file = getFileForKey(key); |
||||
CountingInputStream cis = null; |
||||
try { |
||||
cis = new CountingInputStream(new BufferedInputStream(new FileInputStream(file))); |
||||
CacheHeader.readHeader(cis); // eat header
|
||||
byte[] data = streamToBytes(cis, (int) (file.length() - cis.bytesRead)); |
||||
return entry.toCacheEntry(data); |
||||
} catch (IOException e) { |
||||
VolleyLog.d("%s: %s", file.getAbsolutePath(), e.toString()); |
||||
remove(key); |
||||
return null; |
||||
} finally { |
||||
if (cis != null) { |
||||
try { |
||||
cis.close(); |
||||
} catch (IOException ioe) { |
||||
return null; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Initializes the DiskBasedCache by scanning for all files currently in the |
||||
* specified root directory. Creates the root directory if necessary. |
||||
*/ |
||||
@Override |
||||
public synchronized void initialize() { |
||||
if (!mRootDirectory.exists()) { |
||||
if (!mRootDirectory.mkdirs()) { |
||||
VolleyLog.e("Unable to create cache dir %s", mRootDirectory.getAbsolutePath()); |
||||
} |
||||
return; |
||||
} |
||||
|
||||
File[] files = mRootDirectory.listFiles(); |
||||
if (files == null) { |
||||
return; |
||||
} |
||||
for (File file : files) { |
||||
BufferedInputStream fis = null; |
||||
try { |
||||
fis = new BufferedInputStream(new FileInputStream(file)); |
||||
CacheHeader entry = CacheHeader.readHeader(fis); |
||||
entry.size = file.length(); |
||||
putEntry(entry.key, entry); |
||||
} catch (IOException e) { |
||||
if (file != null) { |
||||
file.delete(); |
||||
} |
||||
} finally { |
||||
try { |
||||
if (fis != null) { |
||||
fis.close(); |
||||
} |
||||
} catch (IOException ignored) { } |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Invalidates an entry in the cache. |
||||
* @param key Cache key |
||||
* @param fullExpire True to fully expire the entry, false to soft expire |
||||
*/ |
||||
@Override |
||||
public synchronized void invalidate(String key, boolean fullExpire) { |
||||
Entry entry = get(key); |
||||
if (entry != null) { |
||||
entry.softTtl = 0; |
||||
if (fullExpire) { |
||||
entry.ttl = 0; |
||||
} |
||||
put(key, entry); |
||||
} |
||||
|
||||
} |
||||
|
||||
/** |
||||
* Puts the entry with the specified key into the cache. |
||||
*/ |
||||
@Override |
||||
public synchronized void put(String key, Entry entry) { |
||||
pruneIfNeeded(entry.data.length); |
||||
File file = getFileForKey(key); |
||||
try { |
||||
BufferedOutputStream fos = new BufferedOutputStream(new FileOutputStream(file)); |
||||
CacheHeader e = new CacheHeader(key, entry); |
||||
boolean success = e.writeHeader(fos); |
||||
if (!success) { |
||||
fos.close(); |
||||
VolleyLog.d("Failed to write header for %s", file.getAbsolutePath()); |
||||
throw new IOException(); |
||||
} |
||||
fos.write(entry.data); |
||||
fos.close(); |
||||
putEntry(key, e); |
||||
return; |
||||
} catch (IOException e) { |
||||
} |
||||
boolean deleted = file.delete(); |
||||
if (!deleted) { |
||||
VolleyLog.d("Could not clean up file %s", file.getAbsolutePath()); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Removes the specified key from the cache if it exists. |
||||
*/ |
||||
@Override |
||||
public synchronized void remove(String key) { |
||||
boolean deleted = getFileForKey(key).delete(); |
||||
removeEntry(key); |
||||
if (!deleted) { |
||||
VolleyLog.d("Could not delete cache entry for key=%s, filename=%s", |
||||
key, getFilenameForKey(key)); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Creates a pseudo-unique filename for the specified cache key. |
||||
* @param key The key to generate a file name for. |
||||
* @return A pseudo-unique filename. |
||||
*/ |
||||
private String getFilenameForKey(String key) { |
||||
int firstHalfLength = key.length() / 2; |
||||
String localFilename = String.valueOf(key.substring(0, firstHalfLength).hashCode()); |
||||
localFilename += String.valueOf(key.substring(firstHalfLength).hashCode()); |
||||
return localFilename; |
||||
} |
||||
|
||||
/** |
||||
* Returns a file object for the given cache key. |
||||
*/ |
||||
public File getFileForKey(String key) { |
||||
return new File(mRootDirectory, getFilenameForKey(key)); |
||||
} |
||||
|
||||
/** |
||||
* Prunes the cache to fit the amount of bytes specified. |
||||
* @param neededSpace The amount of bytes we are trying to fit into the cache. |
||||
*/ |
||||
private void pruneIfNeeded(int neededSpace) { |
||||
if (mEntries.size() <= DEFAULT_DISK_FILES_MAX && (mTotalSize + neededSpace) < mMaxCacheSizeInBytes) { |
||||
return; |
||||
} |
||||
if (VolleyLog.DEBUG) { |
||||
VolleyLog.v("Pruning old cache entries."); |
||||
} |
||||
|
||||
long before = mTotalSize; |
||||
int prunedFiles = 0; |
||||
long startTime = SystemClock.elapsedRealtime(); |
||||
|
||||
Iterator<Map.Entry<String, CacheHeader>> iterator = mEntries.entrySet().iterator(); |
||||
while (iterator.hasNext()) { |
||||
Map.Entry<String, CacheHeader> entry = iterator.next(); |
||||
CacheHeader e = entry.getValue(); |
||||
boolean deleted = getFileForKey(e.key).delete(); |
||||
if (deleted) { |
||||
mTotalSize -= e.size; |
||||
} else { |
||||
VolleyLog.d("Could not delete cache entry for key=%s, filename=%s", |
||||
e.key, getFilenameForKey(e.key)); |
||||
} |
||||
iterator.remove(); |
||||
prunedFiles++; |
||||
|
||||
if (mEntries.size() < DEFAULT_DISK_FILES_MAX * HYSTERESIS_FACTOR && (mTotalSize + neededSpace) < mMaxCacheSizeInBytes * HYSTERESIS_FACTOR) { |
||||
break; |
||||
} |
||||
} |
||||
|
||||
if (VolleyLog.DEBUG) { |
||||
VolleyLog.v("pruned %d files, %d bytes, %d ms", |
||||
prunedFiles, (mTotalSize - before), SystemClock.elapsedRealtime() - startTime); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Puts the entry with the specified key into the cache. |
||||
* @param key The key to identify the entry by. |
||||
* @param entry The entry to cache. |
||||
*/ |
||||
private void putEntry(String key, CacheHeader entry) { |
||||
if (!mEntries.containsKey(key)) { |
||||
mTotalSize += entry.size; |
||||
} else { |
||||
CacheHeader oldEntry = mEntries.get(key); |
||||
mTotalSize += (entry.size - oldEntry.size); |
||||
} |
||||
mEntries.put(key, entry); |
||||
} |
||||
|
||||
/** |
||||
* Removes the entry identified by 'key' from the cache. |
||||
*/ |
||||
private void removeEntry(String key) { |
||||
CacheHeader entry = mEntries.get(key); |
||||
if (entry != null) { |
||||
mTotalSize -= entry.size; |
||||
mEntries.remove(key); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Reads the contents of an InputStream into a byte[]. |
||||
* */ |
||||
private static byte[] streamToBytes(InputStream in, int length) throws IOException { |
||||
if (length < 0) { |
||||
throw new IOException("length < 0"); |
||||
} |
||||
|
||||
byte[] bytes = new byte[length]; |
||||
int count; |
||||
int pos = 0; |
||||
while (pos < length && ((count = in.read(bytes, pos, length - pos)) != -1)) { |
||||
pos += count; |
||||
} |
||||
if (pos != length) { |
||||
throw new IOException("Expected " + length + " bytes, read " + pos + " bytes"); |
||||
} |
||||
return bytes; |
||||
} |
||||
|
||||
/** |
||||
* Handles holding onto the cache headers for an entry. |
||||
*/ |
||||
// Visible for testing.
|
||||
static class CacheHeader { |
||||
/** The size of the data identified by this CacheHeader. (This is not |
||||
* serialized to disk. */ |
||||
public long size; |
||||
|
||||
/** The key that identifies the cache entry. */ |
||||
public String key; |
||||
|
||||
/** ETag for cache coherence. */ |
||||
public String etag; |
||||
|
||||
/** Date of this response as reported by the server. */ |
||||
public long serverDate; |
||||
|
||||
/** TTL for this record. */ |
||||
public long ttl; |
||||
|
||||
/** Soft TTL for this record. */ |
||||
public long softTtl; |
||||
|
||||
/** Headers from the response resulting in this cache entry. */ |
||||
public Map<String, String> responseHeaders; |
||||
|
||||
private CacheHeader() { } |
||||
|
||||
/** |
||||
* Instantiates a new CacheHeader object |
||||
* @param key The key that identifies the cache entry |
||||
* @param entry The cache entry. |
||||
*/ |
||||
public CacheHeader(String key, Entry entry) { |
||||
this.key = key; |
||||
this.size = entry.data.length; |
||||
this.etag = entry.etag; |
||||
this.serverDate = entry.serverDate; |
||||
this.ttl = entry.ttl; |
||||
this.softTtl = entry.softTtl; |
||||
this.responseHeaders = entry.responseHeaders; |
||||
} |
||||
|
||||
/** |
||||
* Reads the header off of an InputStream and returns a CacheHeader object. |
||||
* @param is The InputStream to read from. |
||||
* @throws IOException |
||||
*/ |
||||
public static CacheHeader readHeader(InputStream is) throws IOException { |
||||
CacheHeader entry = new CacheHeader(); |
||||
int magic = readInt(is); |
||||
if (magic != CACHE_MAGIC) { |
||||
// don't bother deleting, it'll get pruned eventually
|
||||
throw new IOException(); |
||||
} |
||||
entry.key = readString(is); |
||||
entry.etag = readString(is); |
||||
if (entry.etag.equals("")) { |
||||
entry.etag = null; |
||||
} |
||||
entry.serverDate = readLong(is); |
||||
entry.ttl = readLong(is); |
||||
entry.softTtl = readLong(is); |
||||
entry.responseHeaders = readStringStringMap(is); |
||||
return entry; |
||||
} |
||||
|
||||
/** |
||||
* Creates a cache entry for the specified data. |
||||
*/ |
||||
public Entry toCacheEntry(byte[] data) { |
||||
Entry e = new Entry(); |
||||
e.data = data; |
||||
e.etag = etag; |
||||
e.serverDate = serverDate; |
||||
e.ttl = ttl; |
||||
e.softTtl = softTtl; |
||||
e.responseHeaders = responseHeaders; |
||||
return e; |
||||
} |
||||
|
||||
|
||||
/** |
||||
* Writes the contents of this CacheHeader to the specified OutputStream. |
||||
*/ |
||||
public boolean writeHeader(OutputStream os) { |
||||
try { |
||||
writeInt(os, CACHE_MAGIC); |
||||
writeString(os, key); |
||||
writeString(os, etag == null ? "" : etag); |
||||
writeLong(os, serverDate); |
||||
writeLong(os, ttl); |
||||
writeLong(os, softTtl); |
||||
writeStringStringMap(responseHeaders, os); |
||||
os.flush(); |
||||
return true; |
||||
} catch (IOException e) { |
||||
VolleyLog.d("%s", e.toString()); |
||||
return false; |
||||
} |
||||
} |
||||
|
||||
} |
||||
|
||||
private static class CountingInputStream extends FilterInputStream { |
||||
private int bytesRead = 0; |
||||
|
||||
private CountingInputStream(InputStream in) { |
||||
super(in); |
||||
} |
||||
|
||||
@Override |
||||
public int read() throws IOException { |
||||
int result = super.read(); |
||||
if (result != -1) { |
||||
bytesRead++; |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
@Override |
||||
public int read(byte[] buffer, int offset, int count) throws IOException { |
||||
int result = super.read(buffer, offset, count); |
||||
if (result != -1) { |
||||
bytesRead += result; |
||||
} |
||||
return result; |
||||
} |
||||
} |
||||
|
||||
/* |
||||
* Homebrewed simple serialization system used for reading and writing cache |
||||
* headers on disk. Once upon a time, this used the standard Java |
||||
* Object{Input,Output}Stream, but the default implementation relies heavily |
||||
* on reflection (even for standard types) and generates a ton of garbage. |
||||
*/ |
||||
|
||||
/** |
||||
* Simple wrapper around {@link InputStream#read()} that throws EOFException |
||||
* instead of returning -1. |
||||
*/ |
||||
private static int read(InputStream is) throws IOException { |
||||
int b = is.read(); |
||||
if (b == -1) { |
||||
throw new EOFException(); |
||||
} |
||||
return b; |
||||
} |
||||
|
||||
static void writeInt(OutputStream os, int n) throws IOException { |
||||
os.write((n >> 0) & 0xff); |
||||
os.write((n >> 8) & 0xff); |
||||
os.write((n >> 16) & 0xff); |
||||
os.write((n >> 24) & 0xff); |
||||
} |
||||
|
||||
static int readInt(InputStream is) throws IOException { |
||||
int n = 0; |
||||
n |= (read(is) << 0); |
||||
n |= (read(is) << 8); |
||||
n |= (read(is) << 16); |
||||
n |= (read(is) << 24); |
||||
return n; |
||||
} |
||||
|
||||
static void writeLong(OutputStream os, long n) throws IOException { |
||||
os.write((byte)(n >>> 0)); |
||||
os.write((byte)(n >>> 8)); |
||||
os.write((byte)(n >>> 16)); |
||||
os.write((byte)(n >>> 24)); |
||||
os.write((byte)(n >>> 32)); |
||||
os.write((byte)(n >>> 40)); |
||||
os.write((byte)(n >>> 48)); |
||||
os.write((byte)(n >>> 56)); |
||||
} |
||||
|
||||
static long readLong(InputStream is) throws IOException { |
||||
long n = 0; |
||||
n |= ((read(is) & 0xFFL) << 0); |
||||
n |= ((read(is) & 0xFFL) << 8); |
||||
n |= ((read(is) & 0xFFL) << 16); |
||||
n |= ((read(is) & 0xFFL) << 24); |
||||
n |= ((read(is) & 0xFFL) << 32); |
||||
n |= ((read(is) & 0xFFL) << 40); |
||||
n |= ((read(is) & 0xFFL) << 48); |
||||
n |= ((read(is) & 0xFFL) << 56); |
||||
return n; |
||||
} |
||||
|
||||
static void writeString(OutputStream os, String s) throws IOException { |
||||
byte[] b = s.getBytes("UTF-8"); |
||||
writeLong(os, b.length); |
||||
os.write(b, 0, b.length); |
||||
} |
||||
|
||||
static String readString(InputStream is) throws IOException { |
||||
int n = (int) readLong(is); |
||||
byte[] b = streamToBytes(is, n); |
||||
return new String(b, "UTF-8"); |
||||
} |
||||
|
||||
static void writeStringStringMap(Map<String, String> map, OutputStream os) throws IOException { |
||||
if (map != null) { |
||||
writeInt(os, map.size()); |
||||
for (Map.Entry<String, String> entry : map.entrySet()) { |
||||
writeString(os, entry.getKey()); |
||||
writeString(os, entry.getValue()); |
||||
} |
||||
} else { |
||||
writeInt(os, 0); |
||||
} |
||||
} |
||||
|
||||
static Map<String, String> readStringStringMap(InputStream is) throws IOException { |
||||
int size = readInt(is); |
||||
Map<String, String> result = (size == 0) |
||||
? Collections.<String, String>emptyMap() |
||||
: new HashMap<String, String>(size); |
||||
for (int i = 0; i < size; i++) { |
||||
String key = readString(is).intern(); |
||||
String value = readString(is).intern(); |
||||
result.put(key, value); |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
|
||||
} |
@ -1,194 +0,0 @@ |
||||
/* |
||||
* Copyright (C) 2011 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.android.volley.toolbox; |
||||
|
||||
import com.android.volley.AuthFailureError; |
||||
import com.android.volley.Request; |
||||
import com.android.volley.Request.Method; |
||||
|
||||
import org.apache.http.HttpEntity; |
||||
import org.apache.http.HttpResponse; |
||||
import org.apache.http.NameValuePair; |
||||
import org.apache.http.client.HttpClient; |
||||
import org.apache.http.client.methods.HttpDelete; |
||||
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; |
||||
import org.apache.http.client.methods.HttpGet; |
||||
import org.apache.http.client.methods.HttpHead; |
||||
import org.apache.http.client.methods.HttpOptions; |
||||
import org.apache.http.client.methods.HttpPost; |
||||
import org.apache.http.client.methods.HttpPut; |
||||
import org.apache.http.client.methods.HttpTrace; |
||||
import org.apache.http.client.methods.HttpUriRequest; |
||||
import org.apache.http.entity.ByteArrayEntity; |
||||
import org.apache.http.message.BasicNameValuePair; |
||||
import org.apache.http.params.HttpConnectionParams; |
||||
import org.apache.http.params.HttpParams; |
||||
|
||||
import java.io.IOException; |
||||
import java.net.URI; |
||||
import java.util.ArrayList; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
|
||||
/** |
||||
* An HttpStack that performs request over an {@link HttpClient}. |
||||
*/ |
||||
public class HttpClientStack implements HttpStack { |
||||
protected final HttpClient mClient; |
||||
|
||||
private final static String HEADER_CONTENT_TYPE = "Content-Type"; |
||||
|
||||
public HttpClientStack(HttpClient client) { |
||||
mClient = client; |
||||
} |
||||
|
||||
private static void addHeaders(HttpUriRequest httpRequest, Map<String, String> headers) { |
||||
for (String key : headers.keySet()) { |
||||
httpRequest.setHeader(key, headers.get(key)); |
||||
} |
||||
} |
||||
|
||||
@SuppressWarnings("unused") |
||||
private static List<NameValuePair> getPostParameterPairs(Map<String, String> postParams) { |
||||
List<NameValuePair> result = new ArrayList<NameValuePair>(postParams.size()); |
||||
for (String key : postParams.keySet()) { |
||||
result.add(new BasicNameValuePair(key, postParams.get(key))); |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
@Override |
||||
public HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders) |
||||
throws IOException, AuthFailureError { |
||||
HttpUriRequest httpRequest = createHttpRequest(request, additionalHeaders); |
||||
addHeaders(httpRequest, additionalHeaders); |
||||
addHeaders(httpRequest, request.getHeaders()); |
||||
onPrepareRequest(httpRequest); |
||||
HttpParams httpParams = httpRequest.getParams(); |
||||
int timeoutMs = request.getTimeoutMs(); |
||||
// TODO: Reevaluate this connection timeout based on more wide-scale
|
||||
// data collection and possibly different for wifi vs. 3G.
|
||||
HttpConnectionParams.setConnectionTimeout(httpParams, 5000); |
||||
HttpConnectionParams.setSoTimeout(httpParams, timeoutMs); |
||||
return mClient.execute(httpRequest); |
||||
} |
||||
|
||||
/** |
||||
* Creates the appropriate subclass of HttpUriRequest for passed in request. |
||||
*/ |
||||
@SuppressWarnings("deprecation") |
||||
/* protected */ static HttpUriRequest createHttpRequest(Request<?> request, |
||||
Map<String, String> additionalHeaders) throws AuthFailureError { |
||||
switch (request.getMethod()) { |
||||
case Method.DEPRECATED_GET_OR_POST: { |
||||
// This is the deprecated way that needs to be handled for backwards compatibility.
|
||||
// If the request's post body is null, then the assumption is that the request is
|
||||
// GET. Otherwise, it is assumed that the request is a POST.
|
||||
byte[] postBody = request.getPostBody(); |
||||
if (postBody != null) { |
||||
HttpPost postRequest = new HttpPost(request.getUrl()); |
||||
postRequest.addHeader(HEADER_CONTENT_TYPE, request.getPostBodyContentType()); |
||||
HttpEntity entity; |
||||
entity = new ByteArrayEntity(postBody); |
||||
postRequest.setEntity(entity); |
||||
return postRequest; |
||||
} else { |
||||
return new HttpGet(request.getUrl()); |
||||
} |
||||
} |
||||
case Method.GET: |
||||
return new HttpGet(request.getUrl()); |
||||
case Method.DELETE: |
||||
return new HttpDelete(request.getUrl()); |
||||
case Method.POST: { |
||||
HttpPost postRequest = new HttpPost(request.getUrl()); |
||||
postRequest.addHeader(HEADER_CONTENT_TYPE, request.getBodyContentType()); |
||||
setEntityIfNonEmptyBody(postRequest, request); |
||||
return postRequest; |
||||
} |
||||
case Method.PUT: { |
||||
HttpPut putRequest = new HttpPut(request.getUrl()); |
||||
putRequest.addHeader(HEADER_CONTENT_TYPE, request.getBodyContentType()); |
||||
setEntityIfNonEmptyBody(putRequest, request); |
||||
return putRequest; |
||||
} |
||||
case Method.HEAD: |
||||
return new HttpHead(request.getUrl()); |
||||
case Method.OPTIONS: |
||||
return new HttpOptions(request.getUrl()); |
||||
case Method.TRACE: |
||||
return new HttpTrace(request.getUrl()); |
||||
case Method.PATCH: { |
||||
HttpPatch patchRequest = new HttpPatch(request.getUrl()); |
||||
patchRequest.addHeader(HEADER_CONTENT_TYPE, request.getBodyContentType()); |
||||
setEntityIfNonEmptyBody(patchRequest, request); |
||||
return patchRequest; |
||||
} |
||||
default: |
||||
throw new IllegalStateException("Unknown request method."); |
||||
} |
||||
} |
||||
|
||||
private static void setEntityIfNonEmptyBody(HttpEntityEnclosingRequestBase httpRequest, |
||||
Request<?> request) throws AuthFailureError { |
||||
byte[] body = request.getBody(); |
||||
if (body != null) { |
||||
HttpEntity entity = new ByteArrayEntity(body); |
||||
httpRequest.setEntity(entity); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Called before the request is executed using the underlying HttpClient. |
||||
* |
||||
* <p>Overwrite in subclasses to augment the request.</p> |
||||
*/ |
||||
protected void onPrepareRequest(HttpUriRequest request) throws IOException { |
||||
// Nothing.
|
||||
} |
||||
|
||||
/** |
||||
* The HttpPatch class does not exist in the Android framework, so this has been defined here. |
||||
*/ |
||||
public static final class HttpPatch extends HttpEntityEnclosingRequestBase { |
||||
|
||||
public final static String METHOD_NAME = "PATCH"; |
||||
|
||||
public HttpPatch() { |
||||
super(); |
||||
} |
||||
|
||||
public HttpPatch(final URI uri) { |
||||
super(); |
||||
setURI(uri); |
||||
} |
||||
|
||||
/** |
||||
* @throws IllegalArgumentException if the uri is invalid. |
||||
*/ |
||||
public HttpPatch(final String uri) { |
||||
super(); |
||||
setURI(URI.create(uri)); |
||||
} |
||||
|
||||
@Override |
||||
public String getMethod() { |
||||
return METHOD_NAME; |
||||
} |
||||
|
||||
} |
||||
} |
@ -1,137 +0,0 @@ |
||||
/* |
||||
* Copyright (C) 2011 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.android.volley.toolbox; |
||||
|
||||
import com.android.volley.Cache; |
||||
import com.android.volley.NetworkResponse; |
||||
|
||||
import org.apache.http.impl.cookie.DateParseException; |
||||
import org.apache.http.impl.cookie.DateUtils; |
||||
import org.apache.http.protocol.HTTP; |
||||
|
||||
import java.util.Map; |
||||
|
||||
/** |
||||
* Utility methods for parsing HTTP headers. |
||||
*/ |
||||
public class HttpHeaderParser { |
||||
|
||||
/** |
||||
* Extracts a {@link Cache.Entry} from a {@link NetworkResponse}. |
||||
* |
||||
* @param response The network response to parse headers from |
||||
* @return a cache entry for the given response, or null if the response is not cacheable. |
||||
*/ |
||||
public static Cache.Entry parseCacheHeaders(NetworkResponse response) { |
||||
long now = System.currentTimeMillis(); |
||||
|
||||
Map<String, String> headers = response.headers; |
||||
|
||||
long serverDate = 0; |
||||
long serverExpires = 0; |
||||
long softExpire = 0; |
||||
long maxAge = 0; |
||||
boolean hasCacheControl = false; |
||||
|
||||
String serverEtag = null; |
||||
String headerValue; |
||||
|
||||
headerValue = headers.get("Date"); |
||||
if (headerValue != null) { |
||||
serverDate = parseDateAsEpoch(headerValue); |
||||
} |
||||
|
||||
headerValue = headers.get("Cache-Control"); |
||||
if (headerValue != null) { |
||||
hasCacheControl = true; |
||||
String[] tokens = headerValue.split(","); |
||||
for (int i = 0; i < tokens.length; i++) { |
||||
String token = tokens[i].trim(); |
||||
if (token.equals("no-cache") || token.equals("no-store")) { |
||||
return null; |
||||
} else if (token.startsWith("max-age=")) { |
||||
try { |
||||
maxAge = Long.parseLong(token.substring(8)); |
||||
} catch (Exception e) { |
||||
} |
||||
} else if (token.equals("must-revalidate") || token.equals("proxy-revalidate")) { |
||||
maxAge = 0; |
||||
} |
||||
} |
||||
} |
||||
|
||||
headerValue = headers.get("Expires"); |
||||
if (headerValue != null) { |
||||
serverExpires = parseDateAsEpoch(headerValue); |
||||
} |
||||
|
||||
serverEtag = headers.get("ETag"); |
||||
|
||||
// Cache-Control takes precedence over an Expires header, even if both exist and Expires
|
||||
// is more restrictive.
|
||||
if (hasCacheControl) { |
||||
softExpire = now + maxAge * 1000; |
||||
} else if (serverDate > 0 && serverExpires >= serverDate) { |
||||
// Default semantic for Expire header in HTTP specification is softExpire.
|
||||
softExpire = now + (serverExpires - serverDate); |
||||
} |
||||
|
||||
Cache.Entry entry = new Cache.Entry(); |
||||
entry.data = response.data; |
||||
entry.etag = serverEtag; |
||||
entry.softTtl = softExpire; |
||||
entry.ttl = entry.softTtl; |
||||
entry.serverDate = serverDate; |
||||
entry.responseHeaders = headers; |
||||
|
||||
return entry; |
||||
} |
||||
|
||||
/** |
||||
* Parse date in RFC1123 format, and return its value as epoch |
||||
*/ |
||||
public static long parseDateAsEpoch(String dateStr) { |
||||
try { |
||||
// Parse date in RFC1123 format if this header contains one
|
||||
return DateUtils.parseDate(dateStr).getTime(); |
||||
} catch (DateParseException e) { |
||||
// Date in invalid format, fallback to 0
|
||||
return 0; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Returns the charset specified in the Content-Type of this header, |
||||
* or the HTTP default (ISO-8859-1) if none can be found. |
||||
*/ |
||||
public static String parseCharset(Map<String, String> headers) { |
||||
String contentType = headers.get(HTTP.CONTENT_TYPE); |
||||
if (contentType != null) { |
||||
String[] params = contentType.split(";"); |
||||
for (int i = 1; i < params.length; i++) { |
||||
String[] pair = params[i].trim().split("="); |
||||
if (pair.length == 2) { |
||||
if (pair[0].equals("charset")) { |
||||
return pair[1]; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
return HTTP.DEFAULT_CONTENT_CHARSET; |
||||
} |
||||
} |
@ -1,45 +0,0 @@ |
||||
/* |
||||
* Copyright (C) 2011 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.android.volley.toolbox; |
||||
|
||||
import com.android.volley.AuthFailureError; |
||||
import com.android.volley.Request; |
||||
|
||||
import org.apache.http.HttpResponse; |
||||
|
||||
import java.io.IOException; |
||||
import java.util.Map; |
||||
|
||||
/** |
||||
* An HTTP stack abstraction. |
||||
*/ |
||||
public interface HttpStack { |
||||
/** |
||||
* Performs an HTTP request with the given parameters. |
||||
* |
||||
* <p>A GET request is sent if request.getPostBody() == null. A POST request is sent otherwise, |
||||
* and the Content-Type header is set to request.getPostBodyContentType().</p> |
||||
* |
||||
* @param request the request to perform |
||||
* @param additionalHeaders additional headers to be sent together with |
||||
* {@link Request#getHeaders()} |
||||
* @return the HTTP response |
||||
*/ |
||||
public HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders) |
||||
throws IOException, AuthFailureError; |
||||
|
||||
} |
@ -1,248 +0,0 @@ |
||||
/* |
||||
* Copyright (C) 2011 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.android.volley.toolbox; |
||||
|
||||
import com.android.volley.AuthFailureError; |
||||
import com.android.volley.Request; |
||||
import com.android.volley.Request.Method; |
||||
|
||||
import org.apache.http.Header; |
||||
import org.apache.http.HttpEntity; |
||||
import org.apache.http.HttpResponse; |
||||
import org.apache.http.ProtocolVersion; |
||||
import org.apache.http.StatusLine; |
||||
import org.apache.http.entity.BasicHttpEntity; |
||||
import org.apache.http.message.BasicHeader; |
||||
import org.apache.http.message.BasicHttpResponse; |
||||
import org.apache.http.message.BasicStatusLine; |
||||
|
||||
import java.io.DataOutputStream; |
||||
import java.io.IOException; |
||||
import java.io.InputStream; |
||||
import java.net.HttpURLConnection; |
||||
import java.net.URL; |
||||
import java.util.HashMap; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.Map.Entry; |
||||
|
||||
import javax.net.ssl.HttpsURLConnection; |
||||
import javax.net.ssl.SSLSocketFactory; |
||||
|
||||
/** |
||||
* An {@link HttpStack} based on {@link HttpURLConnection}. |
||||
*/ |
||||
public class HurlStack implements HttpStack { |
||||
|
||||
private static final String HEADER_CONTENT_TYPE = "Content-Type"; |
||||
private final String mUserAgent; |
||||
|
||||
/** |
||||
* An interface for transforming URLs before use. |
||||
*/ |
||||
public interface UrlRewriter { |
||||
/** |
||||
* Returns a URL to use instead of the provided one, or null to indicate |
||||
* this URL should not be used at all. |
||||
*/ |
||||
public String rewriteUrl(String originalUrl); |
||||
} |
||||
|
||||
private final UrlRewriter mUrlRewriter; |
||||
private final SSLSocketFactory mSslSocketFactory; |
||||
|
||||
public HurlStack(String userAgent) { |
||||
this(userAgent, null); |
||||
} |
||||
|
||||
/** |
||||
* @param urlRewriter Rewriter to use for request URLs |
||||
*/ |
||||
public HurlStack(String userAgent, UrlRewriter urlRewriter) { |
||||
this(userAgent, urlRewriter, null); |
||||
} |
||||
|
||||
/** |
||||
* @param urlRewriter Rewriter to use for request URLs |
||||
* @param sslSocketFactory SSL factory to use for HTTPS connections |
||||
*/ |
||||
public HurlStack(String userAgent, UrlRewriter urlRewriter, SSLSocketFactory sslSocketFactory) { |
||||
mUserAgent = userAgent; |
||||
mUrlRewriter = urlRewriter; |
||||
mSslSocketFactory = sslSocketFactory; |
||||
} |
||||
|
||||
@Override |
||||
public HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders) |
||||
throws IOException, AuthFailureError { |
||||
String url = request.getUrl(); |
||||
HashMap<String, String> map = new HashMap<String, String>(); |
||||
map.putAll(request.getHeaders()); |
||||
map.putAll(additionalHeaders); |
||||
map.put("User-Agent", mUserAgent); |
||||
if (mUrlRewriter != null) { |
||||
String rewritten = mUrlRewriter.rewriteUrl(url); |
||||
if (rewritten == null) { |
||||
throw new IOException("URL blocked by rewriter: " + url); |
||||
} |
||||
url = rewritten; |
||||
} |
||||
URL parsedUrl = new URL(url); |
||||
HttpURLConnection connection = openConnection(parsedUrl, request); |
||||
for (String headerName : map.keySet()) { |
||||
connection.addRequestProperty(headerName, map.get(headerName)); |
||||
} |
||||
setConnectionParametersForRequest(connection, request); |
||||
// Initialize HttpResponse with data from the HttpURLConnection.
|
||||
ProtocolVersion protocolVersion = new ProtocolVersion("HTTP", 1, 1); |
||||
int responseCode = connection.getResponseCode(); |
||||
if (responseCode == -1) { |
||||
// -1 is returned by getResponseCode() if the response code could not be retrieved.
|
||||
// Signal to the caller that something was wrong with the connection.
|
||||
throw new IOException("Could not retrieve response code from HttpUrlConnection."); |
||||
} |
||||
StatusLine responseStatus = new BasicStatusLine(protocolVersion, |
||||
connection.getResponseCode(), connection.getResponseMessage()); |
||||
BasicHttpResponse response = new BasicHttpResponse(responseStatus); |
||||
response.setEntity(entityFromConnection(connection)); |
||||
for (Entry<String, List<String>> header : connection.getHeaderFields().entrySet()) { |
||||
if (header.getKey() != null) { |
||||
Header h = new BasicHeader(header.getKey(), header.getValue().get(0)); |
||||
response.addHeader(h); |
||||
} |
||||
} |
||||
return response; |
||||
} |
||||
|
||||
/** |
||||
* Initializes an {@link HttpEntity} from the given {@link HttpURLConnection}. |
||||
* @param connection |
||||
* @return an HttpEntity populated with data from <code>connection</code>. |
||||
*/ |
||||
private static HttpEntity entityFromConnection(HttpURLConnection connection) { |
||||
BasicHttpEntity entity = new BasicHttpEntity(); |
||||
InputStream inputStream; |
||||
try { |
||||
inputStream = connection.getInputStream(); |
||||
} catch (IOException ioe) { |
||||
inputStream = connection.getErrorStream(); |
||||
} |
||||
entity.setContent(inputStream); |
||||
entity.setContentLength(connection.getContentLength()); |
||||
entity.setContentEncoding(connection.getContentEncoding()); |
||||
entity.setContentType(connection.getContentType()); |
||||
return entity; |
||||
} |
||||
|
||||
/** |
||||
* Create an {@link HttpURLConnection} for the specified {@code url}. |
||||
*/ |
||||
protected HttpURLConnection createConnection(URL url) throws IOException { |
||||
return (HttpURLConnection) url.openConnection(); |
||||
} |
||||
|
||||
/** |
||||
* Opens an {@link HttpURLConnection} with parameters. |
||||
* @param url |
||||
* @return an open connection |
||||
* @throws IOException |
||||
*/ |
||||
private HttpURLConnection openConnection(URL url, Request<?> request) throws IOException { |
||||
HttpURLConnection connection = createConnection(url); |
||||
|
||||
int timeoutMs = request.getTimeoutMs(); |
||||
connection.setConnectTimeout(timeoutMs); |
||||
connection.setReadTimeout(timeoutMs); |
||||
connection.setUseCaches(false); |
||||
connection.setDoInput(true); |
||||
|
||||
// use caller-provided custom SslSocketFactory, if any, for HTTPS
|
||||
if ("https".equals(url.getProtocol()) && mSslSocketFactory != null) { |
||||
((HttpsURLConnection)connection).setSSLSocketFactory(mSslSocketFactory); |
||||
} |
||||
|
||||
return connection; |
||||
} |
||||
|
||||
@SuppressWarnings("deprecation") |
||||
/* package */ static void setConnectionParametersForRequest(HttpURLConnection connection, |
||||
Request<?> request) throws IOException, AuthFailureError { |
||||
switch (request.getMethod()) { |
||||
case Method.DEPRECATED_GET_OR_POST: |
||||
// This is the deprecated way that needs to be handled for backwards compatibility.
|
||||
// If the request's post body is null, then the assumption is that the request is
|
||||
// GET. Otherwise, it is assumed that the request is a POST.
|
||||
byte[] postBody = request.getPostBody(); |
||||
if (postBody != null) { |
||||
// Prepare output. There is no need to set Content-Length explicitly,
|
||||
// since this is handled by HttpURLConnection using the size of the prepared
|
||||
// output stream.
|
||||
connection.setDoOutput(true); |
||||
connection.setRequestMethod("POST"); |
||||
connection.addRequestProperty(HEADER_CONTENT_TYPE, |
||||
request.getPostBodyContentType()); |
||||
DataOutputStream out = new DataOutputStream(connection.getOutputStream()); |
||||
out.write(postBody); |
||||
out.close(); |
||||
} |
||||
break; |
||||
case Method.GET: |
||||
// Not necessary to set the request method because connection defaults to GET but
|
||||
// being explicit here.
|
||||
connection.setRequestMethod("GET"); |
||||
break; |
||||
case Method.DELETE: |
||||
connection.setRequestMethod("DELETE"); |
||||
break; |
||||
case Method.POST: |
||||
connection.setRequestMethod("POST"); |
||||
addBodyIfExists(connection, request); |
||||
break; |
||||
case Method.PUT: |
||||
connection.setRequestMethod("PUT"); |
||||
addBodyIfExists(connection, request); |
||||
break; |
||||
case Method.HEAD: |
||||
connection.setRequestMethod("HEAD"); |
||||
break; |
||||
case Method.OPTIONS: |
||||
connection.setRequestMethod("OPTIONS"); |
||||
break; |
||||
case Method.TRACE: |
||||
connection.setRequestMethod("TRACE"); |
||||
break; |
||||
case Method.PATCH: |
||||
connection.setRequestMethod("PATCH"); |
||||
addBodyIfExists(connection, request); |
||||
break; |
||||
default: |
||||
throw new IllegalStateException("Unknown method type."); |
||||
} |
||||
} |
||||
|
||||
private static void addBodyIfExists(HttpURLConnection connection, Request<?> request) |
||||
throws IOException, AuthFailureError { |
||||
byte[] body = request.getBody(); |
||||
if (body != null) { |
||||
connection.setDoOutput(true); |
||||
connection.addRequestProperty(HEADER_CONTENT_TYPE, request.getBodyContentType()); |
||||
DataOutputStream out = new DataOutputStream(connection.getOutputStream()); |
||||
out.write(body); |
||||
out.close(); |
||||
} |
||||
} |
||||
} |
@ -1,482 +0,0 @@ |
||||
/** |
||||
* Copyright (C) 2013 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package com.android.volley.toolbox; |
||||
|
||||
import android.graphics.Bitmap; |
||||
import android.graphics.Bitmap.Config; |
||||
import android.os.Handler; |
||||
import android.os.Looper; |
||||
import android.widget.ImageView; |
||||
|
||||
import com.android.volley.Request; |
||||
import com.android.volley.RequestQueue; |
||||
import com.android.volley.Response.ErrorListener; |
||||
import com.android.volley.Response.Listener; |
||||
import com.android.volley.VolleyError; |
||||
|
||||
import java.util.HashMap; |
||||
import java.util.LinkedList; |
||||
|
||||
/** |
||||
* Helper that handles loading and caching images from remote URLs. |
||||
* |
||||
* The simple way to use this class is to call {@link ImageLoader#get(String, ImageListener)} |
||||
* and to pass in the default image listener provided by |
||||
* {@link ImageLoader#getImageListener(ImageView, int, int)}. Note that all function calls to |
||||
* this class must be made from the main thead, and all responses will be delivered to the main |
||||
* thread as well. |
||||
*/ |
||||
public class ImageLoader { |
||||
/** RequestQueue for dispatching ImageRequests onto. */ |
||||
private final RequestQueue mRequestQueue; |
||||
|
||||
/** Amount of time to wait after first response arrives before delivering all responses. */ |
||||
private int mBatchResponseDelayMs = 100; |
||||
|
||||
/** The cache implementation to be used as an L1 cache before calling into volley. */ |
||||
private final ImageCache mCache; |
||||
|
||||
/** |
||||
* HashMap of Cache keys -> BatchedImageRequest used to track in-flight requests so |
||||
* that we can coalesce multiple requests to the same URL into a single network request. |
||||
*/ |
||||
private final HashMap<String, BatchedImageRequest> mInFlightRequests = |
||||
new HashMap<String, BatchedImageRequest>(); |
||||
|
||||
/** HashMap of the currently pending responses (waiting to be delivered). */ |
||||
private final HashMap<String, BatchedImageRequest> mBatchedResponses = |
||||
new HashMap<String, BatchedImageRequest>(); |
||||
|
||||
/** Handler to the main thread. */ |
||||
private final Handler mHandler = new Handler(Looper.getMainLooper()); |
||||
|
||||
/** Runnable for in-flight response delivery. */ |
||||
private Runnable mRunnable; |
||||
|
||||
/** |
||||
* Simple cache adapter interface. If provided to the ImageLoader, it |
||||
* will be used as an L1 cache before dispatch to Volley. Implementations |
||||
* must not block. Implementation with an LruCache is recommended. |
||||
*/ |
||||
public interface ImageCache { |
||||
public Bitmap getBitmap(String url); |
||||
public void putBitmap(String url, Bitmap bitmap); |
||||
} |
||||
|
||||
/** |
||||
* Constructs a new ImageLoader. |
||||
* @param queue The RequestQueue to use for making image requests. |
||||
* @param imageCache The cache to use as an L1 cache. |
||||
*/ |
||||
public ImageLoader(RequestQueue queue, ImageCache imageCache) { |
||||
mRequestQueue = queue; |
||||
mCache = imageCache; |
||||
} |
||||
|
||||
/** |
||||
* The default implementation of ImageListener which handles basic functionality |
||||
* of showing a default image until the network response is received, at which point |
||||
* it will switch to either the actual image or the error image. |
||||
* @param imageView The imageView that the listener is associated with. |
||||
* @param defaultImageResId Default image resource ID to use, or 0 if it doesn't exist. |
||||
* @param errorImageResId Error image resource ID to use, or 0 if it doesn't exist. |
||||
*/ |
||||
public static ImageListener getImageListener(final ImageView view, |
||||
final int defaultImageResId, final int errorImageResId) { |
||||
return new ImageListener() { |
||||
@Override |
||||
public void onErrorResponse(VolleyError error) { |
||||
if (errorImageResId != 0) { |
||||
view.setImageResource(errorImageResId); |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public void onResponse(ImageContainer response, boolean isImmediate) { |
||||
if (response.getBitmap() != null) { |
||||
view.setImageBitmap(response.getBitmap()); |
||||
} else if (defaultImageResId != 0) { |
||||
view.setImageResource(defaultImageResId); |
||||
} |
||||
} |
||||
}; |
||||
} |
||||
|
||||
/** |
||||
* Interface for the response handlers on image requests. |
||||
* |
||||
* The call flow is this: |
||||
* 1. Upon being attached to a request, onResponse(response, true) will |
||||
* be invoked to reflect any cached data that was already available. If the |
||||
* data was available, response.getBitmap() will be non-null. |
||||
* |
||||
* 2. After a network response returns, only one of the following cases will happen: |
||||
* - onResponse(response, false) will be called if the image was loaded. |
||||
* or |
||||
* - onErrorResponse will be called if there was an error loading the image. |
||||
*/ |
||||
public interface ImageListener extends ErrorListener { |
||||
/** |
||||
* Listens for non-error changes to the loading of the image request. |
||||
* |
||||
* @param response Holds all information pertaining to the request, as well |
||||
* as the bitmap (if it is loaded). |
||||
* @param isImmediate True if this was called during ImageLoader.get() variants. |
||||
* This can be used to differentiate between a cached image loading and a network |
||||
* image loading in order to, for example, run an animation to fade in network loaded |
||||
* images. |
||||
*/ |
||||
public void onResponse(ImageContainer response, boolean isImmediate); |
||||
} |
||||
|
||||
/** |
||||
* Checks if the item is available in the cache. |
||||
* @param requestUrl The url of the remote image |
||||
* @param maxWidth The maximum width of the returned image. |
||||
* @param maxHeight The maximum height of the returned image. |
||||
* @return True if the item exists in cache, false otherwise. |
||||
*/ |
||||
public boolean isCached(String requestUrl, int maxWidth, int maxHeight) { |
||||
throwIfNotOnMainThread(); |
||||
|
||||
String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight); |
||||
return mCache.getBitmap(cacheKey) != null; |
||||
} |
||||
|
||||
/** |
||||
* Returns an ImageContainer for the requested URL. |
||||
* |
||||
* The ImageContainer will contain either the specified default bitmap or the loaded bitmap. |
||||
* If the default was returned, the {@link ImageLoader} will be invoked when the |
||||
* request is fulfilled. |
||||
* |
||||
* @param requestUrl The URL of the image to be loaded. |
||||
* @param defaultImage Optional default image to return until the actual image is loaded. |
||||
*/ |
||||
public ImageContainer get(String requestUrl, final ImageListener listener) { |
||||
return get(requestUrl, listener, 0, 0); |
||||
} |
||||
|
||||
/** |
||||
* Issues a bitmap request with the given URL if that image is not available |
||||
* in the cache, and returns a bitmap container that contains all of the data |
||||
* relating to the request (as well as the default image if the requested |
||||
* image is not available). |
||||
* @param requestUrl The url of the remote image |
||||
* @param imageListener The listener to call when the remote image is loaded |
||||
* @param maxWidth The maximum width of the returned image. |
||||
* @param maxHeight The maximum height of the returned image. |
||||
* @return A container object that contains all of the properties of the request, as well as |
||||
* the currently available image (default if remote is not loaded). |
||||
*/ |
||||
public ImageContainer get(String requestUrl, ImageListener imageListener, |
||||
int maxWidth, int maxHeight) { |
||||
// only fulfill requests that were initiated from the main thread.
|
||||
throwIfNotOnMainThread(); |
||||
|
||||
final String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight); |
||||
|
||||
// Try to look up the request in the cache of remote images.
|
||||
Bitmap cachedBitmap = mCache.getBitmap(cacheKey); |
||||
if (cachedBitmap != null) { |
||||
// Return the cached bitmap.
|
||||
ImageContainer container = new ImageContainer(cachedBitmap, requestUrl, null, null); |
||||
imageListener.onResponse(container, true); |
||||
return container; |
||||
} |
||||
|
||||
// The bitmap did not exist in the cache, fetch it!
|
||||
ImageContainer imageContainer = |
||||
new ImageContainer(null, requestUrl, cacheKey, imageListener); |
||||
|
||||
// Update the caller to let them know that they should use the default bitmap.
|
||||
imageListener.onResponse(imageContainer, true); |
||||
|
||||
// Check to see if a request is already in-flight.
|
||||
BatchedImageRequest request = mInFlightRequests.get(cacheKey); |
||||
if (request != null) { |
||||
// If it is, add this request to the list of listeners.
|
||||
request.addContainer(imageContainer); |
||||
return imageContainer; |
||||
} |
||||
|
||||
// The request is not already in flight. Send the new request to the network and
|
||||
// track it.
|
||||
Request<Bitmap> newRequest = makeImageRequest(requestUrl, maxWidth, maxHeight, cacheKey); |
||||
|
||||
mRequestQueue.add(newRequest); |
||||
mInFlightRequests.put(cacheKey, |
||||
new BatchedImageRequest(newRequest, imageContainer)); |
||||
return imageContainer; |
||||
} |
||||
|
||||
protected Request<Bitmap> makeImageRequest(String requestUrl, int maxWidth, int maxHeight, final String cacheKey) { |
||||
return new ImageRequest(requestUrl, new Listener<Bitmap>() { |
||||
@Override |
||||
public void onResponse(Bitmap response) { |
||||
onGetImageSuccess(cacheKey, response); |
||||
} |
||||
}, maxWidth, maxHeight, |
||||
Config.RGB_565, new ErrorListener() { |
||||
@Override |
||||
public void onErrorResponse(VolleyError error) { |
||||
onGetImageError(cacheKey, error); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* Sets the amount of time to wait after the first response arrives before delivering all |
||||
* responses. Batching can be disabled entirely by passing in 0. |
||||
* @param newBatchedResponseDelayMs The time in milliseconds to wait. |
||||
*/ |
||||
public void setBatchedResponseDelay(int newBatchedResponseDelayMs) { |
||||
mBatchResponseDelayMs = newBatchedResponseDelayMs; |
||||
} |
||||
|
||||
/** |
||||
* Handler for when an image was successfully loaded. |
||||
* @param cacheKey The cache key that is associated with the image request. |
||||
* @param response The bitmap that was returned from the network. |
||||
*/ |
||||
protected void onGetImageSuccess(String cacheKey, Bitmap response) { |
||||
// cache the image that was fetched.
|
||||
mCache.putBitmap(cacheKey, response); |
||||
|
||||
// remove the request from the list of in-flight requests.
|
||||
BatchedImageRequest request = mInFlightRequests.remove(cacheKey); |
||||
|
||||
if (request != null) { |
||||
// Update the response bitmap.
|
||||
request.mResponseBitmap = response; |
||||
|
||||
// Send the batched response
|
||||
batchResponse(cacheKey, request); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Handler for when an image failed to load. |
||||
* @param cacheKey The cache key that is associated with the image request. |
||||
*/ |
||||
protected void onGetImageError(String cacheKey, VolleyError error) { |
||||
// Notify the requesters that something failed via a null result.
|
||||
// Remove this request from the list of in-flight requests.
|
||||
BatchedImageRequest request = mInFlightRequests.remove(cacheKey); |
||||
|
||||
if (request != null) { |
||||
// Set the error for this request
|
||||
request.setError(error); |
||||
|
||||
// Send the batched response
|
||||
batchResponse(cacheKey, request); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Container object for all of the data surrounding an image request. |
||||
*/ |
||||
public class ImageContainer { |
||||
/** |
||||
* The most relevant bitmap for the container. If the image was in cache, the |
||||
* Holder to use for the final bitmap (the one that pairs to the requested URL). |
||||
*/ |
||||
private Bitmap mBitmap; |
||||
|
||||
private final ImageListener mListener; |
||||
|
||||
/** The cache key that was associated with the request */ |
||||
private final String mCacheKey; |
||||
|
||||
/** The request URL that was specified */ |
||||
private final String mRequestUrl; |
||||
|
||||
/** |
||||
* Constructs a BitmapContainer object. |
||||
* @param bitmap The final bitmap (if it exists). |
||||
* @param requestUrl The requested URL for this container. |
||||
* @param cacheKey The cache key that identifies the requested URL for this container. |
||||
*/ |
||||
public ImageContainer(Bitmap bitmap, String requestUrl, |
||||
String cacheKey, ImageListener listener) { |
||||
mBitmap = bitmap; |
||||
mRequestUrl = requestUrl; |
||||
mCacheKey = cacheKey; |
||||
mListener = listener; |
||||
} |
||||
|
||||
/** |
||||
* Releases interest in the in-flight request (and cancels it if no one else is listening). |
||||
*/ |
||||
public void cancelRequest() { |
||||
if (mListener == null) { |
||||
return; |
||||
} |
||||
|
||||
BatchedImageRequest request = mInFlightRequests.get(mCacheKey); |
||||
if (request != null) { |
||||
boolean canceled = request.removeContainerAndCancelIfNecessary(this); |
||||
if (canceled) { |
||||
mInFlightRequests.remove(mCacheKey); |
||||
} |
||||
} else { |
||||
// check to see if it is already batched for delivery.
|
||||
request = mBatchedResponses.get(mCacheKey); |
||||
if (request != null) { |
||||
request.removeContainerAndCancelIfNecessary(this); |
||||
if (request.mContainers.size() == 0) { |
||||
mBatchedResponses.remove(mCacheKey); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Returns the bitmap associated with the request URL if it has been loaded, null otherwise. |
||||
*/ |
||||
public Bitmap getBitmap() { |
||||
return mBitmap; |
||||
} |
||||
|
||||
/** |
||||
* Returns the requested URL for this container. |
||||
*/ |
||||
public String getRequestUrl() { |
||||
return mRequestUrl; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Wrapper class used to map a Request to the set of active ImageContainer objects that are |
||||
* interested in its results. |
||||
*/ |
||||
private class BatchedImageRequest { |
||||
/** The request being tracked */ |
||||
private final Request<?> mRequest; |
||||
|
||||
/** The result of the request being tracked by this item */ |
||||
private Bitmap mResponseBitmap; |
||||
|
||||
/** Error if one occurred for this response */ |
||||
private VolleyError mError; |
||||
|
||||
/** List of all of the active ImageContainers that are interested in the request */ |
||||
private final LinkedList<ImageContainer> mContainers = new LinkedList<ImageContainer>(); |
||||
|
||||
/** |
||||
* Constructs a new BatchedImageRequest object |
||||
* @param request The request being tracked |
||||
* @param container The ImageContainer of the person who initiated the request. |
||||
*/ |
||||
public BatchedImageRequest(Request<?> request, ImageContainer container) { |
||||
mRequest = request; |
||||
mContainers.add(container); |
||||
} |
||||
|
||||
/** |
||||
* Set the error for this response |
||||
*/ |
||||
public void setError(VolleyError error) { |
||||
mError = error; |
||||
} |
||||
|
||||
/** |
||||
* Get the error for this response |
||||
*/ |
||||
public VolleyError getError() { |
||||
return mError; |
||||
} |
||||
|
||||
/** |
||||
* Adds another ImageContainer to the list of those interested in the results of |
||||
* the request. |
||||
*/ |
||||
public void addContainer(ImageContainer container) { |
||||
mContainers.add(container); |
||||
} |
||||
|
||||
/** |
||||
* Detatches the bitmap container from the request and cancels the request if no one is |
||||
* left listening. |
||||
* @param container The container to remove from the list |
||||
* @return True if the request was canceled, false otherwise. |
||||
*/ |
||||
public boolean removeContainerAndCancelIfNecessary(ImageContainer container) { |
||||
mContainers.remove(container); |
||||
if (mContainers.size() == 0) { |
||||
mRequest.cancel(); |
||||
return true; |
||||
} |
||||
return false; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Starts the runnable for batched delivery of responses if it is not already started. |
||||
* @param cacheKey The cacheKey of the response being delivered. |
||||
* @param request The BatchedImageRequest to be delivered. |
||||
* @param error The volley error associated with the request (if applicable). |
||||
*/ |
||||
private void batchResponse(String cacheKey, BatchedImageRequest request) { |
||||
mBatchedResponses.put(cacheKey, request); |
||||
// If we don't already have a batch delivery runnable in flight, make a new one.
|
||||
// Note that this will be used to deliver responses to all callers in mBatchedResponses.
|
||||
if (mRunnable == null) { |
||||
mRunnable = new Runnable() { |
||||
@Override |
||||
public void run() { |
||||
for (BatchedImageRequest bir : mBatchedResponses.values()) { |
||||
for (ImageContainer container : bir.mContainers) { |
||||
// If one of the callers in the batched request canceled the request
|
||||
// after the response was received but before it was delivered,
|
||||
// skip them.
|
||||
if (container.mListener == null) { |
||||
continue; |
||||
} |
||||
if (bir.getError() == null) { |
||||
container.mBitmap = bir.mResponseBitmap; |
||||
container.mListener.onResponse(container, false); |
||||
} else { |
||||
container.mListener.onErrorResponse(bir.getError()); |
||||
} |
||||
} |
||||
} |
||||
mBatchedResponses.clear(); |
||||
mRunnable = null; |
||||
} |
||||
|
||||
}; |
||||
// Post the runnable.
|
||||
mHandler.postDelayed(mRunnable, mBatchResponseDelayMs); |
||||
} |
||||
} |
||||
|
||||
private void throwIfNotOnMainThread() { |
||||
if (Looper.myLooper() != Looper.getMainLooper()) { |
||||
throw new IllegalStateException("ImageLoader must be invoked from the main thread."); |
||||
} |
||||
} |
||||
/** |
||||
* Creates a cache key for use with the L1 cache. |
||||
* @param url The URL of the request. |
||||
* @param maxWidth The max-width of the output. |
||||
* @param maxHeight The max-height of the output. |
||||
*/ |
||||
private static String getCacheKey(String url, int maxWidth, int maxHeight) { |
||||
return new StringBuilder(url.length() + 12).append("#W").append(maxWidth) |
||||
.append("#H").append(maxHeight).append(url).toString(); |
||||
} |
||||
} |
@ -1,217 +0,0 @@ |
||||
/* |
||||
* Copyright (C) 2011 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.android.volley.toolbox; |
||||
|
||||
import android.graphics.Bitmap; |
||||
import android.graphics.Bitmap.Config; |
||||
import android.graphics.BitmapFactory; |
||||
|
||||
import com.android.volley.DefaultRetryPolicy; |
||||
import com.android.volley.NetworkResponse; |
||||
import com.android.volley.ParseError; |
||||
import com.android.volley.Request; |
||||
import com.android.volley.Response; |
||||
import com.android.volley.VolleyLog; |
||||
|
||||
/** |
||||
* A canned request for getting an image at a given URL and calling |
||||
* back with a decoded Bitmap. |
||||
*/ |
||||
public class ImageRequest extends Request<Bitmap> { |
||||
/** Socket timeout in milliseconds for image requests */ |
||||
private static final int IMAGE_TIMEOUT_MS = 1000; |
||||
|
||||
/** Default number of retries for image requests */ |
||||
private static final int IMAGE_MAX_RETRIES = 2; |
||||
|
||||
/** Default backoff multiplier for image requests */ |
||||
private static final float IMAGE_BACKOFF_MULT = 2f; |
||||
|
||||
private final Response.Listener<Bitmap> mListener; |
||||
private final Config mDecodeConfig; |
||||
private final int mMaxWidth; |
||||
private final int mMaxHeight; |
||||
|
||||
/** Decoding lock so that we don't decode more than one image at a time (to avoid OOM's) */ |
||||
private static final Object sDecodeLock = new Object(); |
||||
|
||||
/** |
||||
* Creates a new image request, decoding to a maximum specified width and |
||||
* height. If both width and height are zero, the image will be decoded to |
||||
* its natural size. If one of the two is nonzero, that dimension will be |
||||
* clamped and the other one will be set to preserve the image's aspect |
||||
* ratio. If both width and height are nonzero, the image will be decoded to |
||||
* be fit in the rectangle of dimensions width x height while keeping its |
||||
* aspect ratio. |
||||
* |
||||
* @param url URL of the image |
||||
* @param listener Listener to receive the decoded bitmap |
||||
* @param maxWidth Maximum width to decode this bitmap to, or zero for none |
||||
* @param maxHeight Maximum height to decode this bitmap to, or zero for |
||||
* none |
||||
* @param decodeConfig Format to decode the bitmap to |
||||
* @param errorListener Error listener, or null to ignore errors |
||||
*/ |
||||
public ImageRequest(String url, Response.Listener<Bitmap> listener, int maxWidth, int maxHeight, |
||||
Config decodeConfig, Response.ErrorListener errorListener) { |
||||
super(Method.GET, url, errorListener); |
||||
setRetryPolicy( |
||||
new DefaultRetryPolicy(IMAGE_TIMEOUT_MS, IMAGE_MAX_RETRIES, IMAGE_BACKOFF_MULT)); |
||||
mListener = listener; |
||||
mDecodeConfig = decodeConfig; |
||||
mMaxWidth = maxWidth; |
||||
mMaxHeight = maxHeight; |
||||
} |
||||
|
||||
@Override |
||||
public Priority getPriority() { |
||||
return Priority.LOW; |
||||
} |
||||
|
||||
/** |
||||
* Scales one side of a rectangle to fit aspect ratio. |
||||
* |
||||
* @param maxPrimary Maximum size of the primary dimension (i.e. width for |
||||
* max width), or zero to maintain aspect ratio with secondary |
||||
* dimension |
||||
* @param maxSecondary Maximum size of the secondary dimension, or zero to |
||||
* maintain aspect ratio with primary dimension |
||||
* @param actualPrimary Actual size of the primary dimension |
||||
* @param actualSecondary Actual size of the secondary dimension |
||||
*/ |
||||
private static int getResizedDimension(int maxPrimary, int maxSecondary, int actualPrimary, |
||||
int actualSecondary) { |
||||
// If no dominant value at all, just return the actual.
|
||||
if (maxPrimary == 0 && maxSecondary == 0) { |
||||
return actualPrimary; |
||||
} |
||||
|
||||
// If primary is unspecified, scale primary to match secondary's scaling ratio.
|
||||
if (maxPrimary == 0) { |
||||
double ratio = (double) maxSecondary / (double) actualSecondary; |
||||
return (int) (actualPrimary * ratio); |
||||
} |
||||
|
||||
if (maxSecondary == 0) { |
||||
return maxPrimary; |
||||
} |
||||
|
||||
double ratio = (double) actualSecondary / (double) actualPrimary; |
||||
int resized = maxPrimary; |
||||
if (resized * ratio > maxSecondary) { |
||||
resized = (int) (maxSecondary / ratio); |
||||
} |
||||
return resized; |
||||
} |
||||
|
||||
@Override |
||||
protected Response<Bitmap> parseNetworkResponse(NetworkResponse response) { |
||||
// Serialize all decode on a global lock to reduce concurrent heap usage.
|
||||
synchronized (sDecodeLock) { |
||||
try { |
||||
return doParse(response); |
||||
} catch (OutOfMemoryError e) { |
||||
VolleyLog.e("Caught OOM for %d byte image, url=%s", response.data.length, getUrl()); |
||||
return Response.error(new ParseError(e)); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* The real guts of parseNetworkResponse. Broken out for readability. |
||||
*/ |
||||
private Response<Bitmap> doParse(NetworkResponse response) { |
||||
byte[] data = response.data; |
||||
BitmapFactory.Options decodeOptions = new BitmapFactory.Options(); |
||||
Bitmap bitmap = null; |
||||
if (mMaxWidth == 0 && mMaxHeight == 0) { |
||||
decodeOptions.inPreferredConfig = mDecodeConfig; |
||||
bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions); |
||||
} else { |
||||
// If we have to resize this image, first get the natural bounds.
|
||||
decodeOptions.inJustDecodeBounds = true; |
||||
BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions); |
||||
int actualWidth = decodeOptions.outWidth; |
||||
int actualHeight = decodeOptions.outHeight; |
||||
|
||||
// Then compute the dimensions we would ideally like to decode to.
|
||||
int desiredWidth = getResizedDimension(mMaxWidth, mMaxHeight, |
||||
actualWidth, actualHeight); |
||||
int desiredHeight = getResizedDimension(mMaxHeight, mMaxWidth, |
||||
actualHeight, actualWidth); |
||||
|
||||
// Decode to the nearest power of two scaling factor.
|
||||
decodeOptions.inJustDecodeBounds = false; |
||||
// TODO(ficus): Do we need this or is it okay since API 8 doesn't support it?
|
||||
// decodeOptions.inPreferQualityOverSpeed = PREFER_QUALITY_OVER_SPEED;
|
||||
decodeOptions.inSampleSize = |
||||
findBestSampleSize(actualWidth, actualHeight, desiredWidth, desiredHeight); |
||||
Bitmap tempBitmap = |
||||
BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions); |
||||
|
||||
// Disallow dos by checking the size of the tempBitmap, otherwise the bitmap
|
||||
// constructor will throw an IllegalArgumentException.
|
||||
if (tempBitmap != null && |
||||
(tempBitmap.getWidth() == 0 || tempBitmap.getHeight() == 0)) { |
||||
bitmap = null; |
||||
tempBitmap.recycle(); |
||||
} else if (tempBitmap != null && (tempBitmap.getWidth() > desiredWidth || |
||||
tempBitmap.getHeight() > desiredHeight)) { |
||||
// If necessary, scale down to the maximal acceptable size.
|
||||
bitmap = Bitmap.createScaledBitmap(tempBitmap, |
||||
desiredWidth, desiredHeight, true); |
||||
tempBitmap.recycle(); |
||||
} else { |
||||
bitmap = tempBitmap; |
||||
} |
||||
} |
||||
|
||||
if (bitmap == null) { |
||||
return Response.error(new ParseError(response)); |
||||
} else { |
||||
return Response.success(bitmap, HttpHeaderParser.parseCacheHeaders(response)); |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
protected void deliverResponse(Bitmap response) { |
||||
mListener.onResponse(response); |
||||
} |
||||
|
||||
/** |
||||
* Returns the largest power-of-two divisor for use in downscaling a bitmap |
||||
* that will not result in the scaling past the desired dimensions. |
||||
* |
||||
* @param actualWidth Actual width of the bitmap |
||||
* @param actualHeight Actual height of the bitmap |
||||
* @param desiredWidth Desired width of the bitmap |
||||
* @param desiredHeight Desired height of the bitmap |
||||
*/ |
||||
// Visible for testing.
|
||||
static int findBestSampleSize( |
||||
int actualWidth, int actualHeight, int desiredWidth, int desiredHeight) { |
||||
double wr = (double) actualWidth / desiredWidth; |
||||
double hr = (double) actualHeight / desiredHeight; |
||||
double ratio = Math.min(wr, hr); |
||||
float n = 1.0f; |
||||
while ((n * 2) <= ratio) { |
||||
n *= 2; |
||||
} |
||||
|
||||
return (int) n; |
||||
} |
||||
} |
@ -1,58 +0,0 @@ |
||||
/* |
||||
* Copyright (C) 2011 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.android.volley.toolbox; |
||||
|
||||
import com.android.volley.NetworkResponse; |
||||
import com.android.volley.ParseError; |
||||
import com.android.volley.Response; |
||||
import com.android.volley.Response.ErrorListener; |
||||
import com.android.volley.Response.Listener; |
||||
|
||||
import org.json.JSONArray; |
||||
import org.json.JSONException; |
||||
|
||||
import java.io.UnsupportedEncodingException; |
||||
|
||||
/** |
||||
* A request for retrieving a {@link JSONArray} response body at a given URL. |
||||
*/ |
||||
public class JsonArrayRequest extends JsonRequest<JSONArray> { |
||||
|
||||
/** |
||||
* Creates a new request. |
||||
* @param url URL to fetch the JSON from |
||||
* @param listener Listener to receive the JSON response |
||||
* @param errorListener Error listener, or null to ignore errors. |
||||
*/ |
||||
public JsonArrayRequest(String url, Listener<JSONArray> listener, ErrorListener errorListener) { |
||||
super(Method.GET, url, null, listener, errorListener); |
||||
} |
||||
|
||||
@Override |
||||
protected Response<JSONArray> parseNetworkResponse(NetworkResponse response) { |
||||
try { |
||||
String jsonString = |
||||
new String(response.data, HttpHeaderParser.parseCharset(response.headers)); |
||||
return Response.success(new JSONArray(jsonString), |
||||
HttpHeaderParser.parseCacheHeaders(response)); |
||||
} catch (UnsupportedEncodingException e) { |
||||
return Response.error(new ParseError(e)); |
||||
} catch (JSONException je) { |
||||
return Response.error(new ParseError(je)); |
||||
} |
||||
} |
||||
} |
@ -1,76 +0,0 @@ |
||||
/* |
||||
* Copyright (C) 2011 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.android.volley.toolbox; |
||||
|
||||
import com.android.volley.NetworkResponse; |
||||
import com.android.volley.ParseError; |
||||
import com.android.volley.Response; |
||||
import com.android.volley.Response.ErrorListener; |
||||
import com.android.volley.Response.Listener; |
||||
|
||||
import org.json.JSONException; |
||||
import org.json.JSONObject; |
||||
|
||||
import java.io.UnsupportedEncodingException; |
||||
|
||||
/** |
||||
* A request for retrieving a {@link JSONObject} response body at a given URL, allowing for an |
||||
* optional {@link JSONObject} to be passed in as part of the request body. |
||||
*/ |
||||
public class JsonObjectRequest extends JsonRequest<JSONObject> { |
||||
|
||||
/** |
||||
* Creates a new request. |
||||
* @param method the HTTP method to use |
||||
* @param url URL to fetch the JSON from |
||||
* @param jsonRequest A {@link JSONObject} to post with the request. Null is allowed and |
||||
* indicates no parameters will be posted along with request. |
||||
* @param listener Listener to receive the JSON response |
||||
* @param errorListener Error listener, or null to ignore errors. |
||||
*/ |
||||
public JsonObjectRequest(int method, String url, JSONObject jsonRequest, |
||||
Listener<JSONObject> listener, ErrorListener errorListener) { |
||||
super(method, url, (jsonRequest == null) ? null : jsonRequest.toString(), listener, |
||||
errorListener); |
||||
} |
||||
|
||||
/** |
||||
* Constructor which defaults to <code>GET</code> if <code>jsonRequest</code> is |
||||
* <code>null</code>, <code>POST</code> otherwise. |
||||
* |
||||
* @see #JsonObjectRequest(int, String, JSONObject, Listener, ErrorListener) |
||||
*/ |
||||
public JsonObjectRequest(String url, JSONObject jsonRequest, Listener<JSONObject> listener, |
||||
ErrorListener errorListener) { |
||||
this(jsonRequest == null ? Method.GET : Method.POST, url, jsonRequest, |
||||
listener, errorListener); |
||||
} |
||||
|
||||
@Override |
||||
protected Response<JSONObject> parseNetworkResponse(NetworkResponse response) { |
||||
try { |
||||
String jsonString = |
||||
new String(response.data, HttpHeaderParser.parseCharset(response.headers)); |
||||
return Response.success(new JSONObject(jsonString), |
||||
HttpHeaderParser.parseCacheHeaders(response)); |
||||
} catch (UnsupportedEncodingException e) { |
||||
return Response.error(new ParseError(e)); |
||||
} catch (JSONException je) { |
||||
return Response.error(new ParseError(je)); |
||||
} |
||||
} |
||||
} |
@ -1,102 +0,0 @@ |
||||
/* |
||||
* Copyright (C) 2011 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.android.volley.toolbox; |
||||
|
||||
import com.android.volley.NetworkResponse; |
||||
import com.android.volley.Request; |
||||
import com.android.volley.Response; |
||||
import com.android.volley.Response.ErrorListener; |
||||
import com.android.volley.Response.Listener; |
||||
import com.android.volley.VolleyLog; |
||||
|
||||
import java.io.UnsupportedEncodingException; |
||||
|
||||
/** |
||||
* A request for retrieving a T type response body at a given URL that also |
||||
* optionally sends along a JSON body in the request specified. |
||||
* |
||||
* @param <T> JSON type of response expected |
||||
*/ |
||||
public abstract class JsonRequest<T> extends Request<T> { |
||||
/** Charset for request. */ |
||||
private static final String PROTOCOL_CHARSET = "utf-8"; |
||||
|
||||
/** Content type for request. */ |
||||
private static final String PROTOCOL_CONTENT_TYPE = |
||||
String.format("application/json; charset=%s", PROTOCOL_CHARSET); |
||||
|
||||
private final Listener<T> mListener; |
||||
private final String mRequestBody; |
||||
|
||||
/** |
||||
* Deprecated constructor for a JsonRequest which defaults to GET unless {@link #getPostBody()} |
||||
* or {@link #getPostParams()} is overridden (which defaults to POST). |
||||
* |
||||
* @deprecated Use {@link #JsonRequest(int, String, String, Listener, ErrorListener)}. |
||||
*/ |
||||
public JsonRequest(String url, String requestBody, Listener<T> listener, |
||||
ErrorListener errorListener) { |
||||
this(Method.DEPRECATED_GET_OR_POST, url, requestBody, listener, errorListener); |
||||
} |
||||
|
||||
public JsonRequest(int method, String url, String requestBody, Listener<T> listener, |
||||
ErrorListener errorListener) { |
||||
super(method, url, errorListener); |
||||
mListener = listener; |
||||
mRequestBody = requestBody; |
||||
} |
||||
|
||||
@Override |
||||
protected void deliverResponse(T response) { |
||||
mListener.onResponse(response); |
||||
} |
||||
|
||||
@Override |
||||
abstract protected Response<T> parseNetworkResponse(NetworkResponse response); |
||||
|
||||
/** |
||||
* @deprecated Use {@link #getBodyContentType()}. |
||||
*/ |
||||
@Override |
||||
public String getPostBodyContentType() { |
||||
return getBodyContentType(); |
||||
} |
||||
|
||||
/** |
||||
* @deprecated Use {@link #getBody()}. |
||||
*/ |
||||
@Override |
||||
public byte[] getPostBody() { |
||||
return getBody(); |
||||
} |
||||
|
||||
@Override |
||||
public String getBodyContentType() { |
||||
return PROTOCOL_CONTENT_TYPE; |
||||
} |
||||
|
||||
@Override |
||||
public byte[] getBody() { |
||||
try { |
||||
return mRequestBody == null ? null : mRequestBody.getBytes(PROTOCOL_CHARSET); |
||||
} catch (UnsupportedEncodingException uee) { |
||||
VolleyLog.wtf("Unsupported Encoding while trying to get the bytes of %s using %s", |
||||
mRequestBody, PROTOCOL_CHARSET); |
||||
return null; |
||||
} |
||||
} |
||||
} |
@ -1,219 +0,0 @@ |
||||
/** |
||||
* Copyright (C) 2013 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package com.android.volley.toolbox; |
||||
|
||||
import android.content.Context; |
||||
import android.text.TextUtils; |
||||
import android.util.AttributeSet; |
||||
import android.view.ViewGroup.LayoutParams; |
||||
import android.widget.ImageView; |
||||
|
||||
import com.android.volley.VolleyError; |
||||
import com.android.volley.toolbox.ImageLoader.ImageContainer; |
||||
import com.android.volley.toolbox.ImageLoader.ImageListener; |
||||
|
||||
/** |
||||
* Handles fetching an image from a URL as well as the life-cycle of the |
||||
* associated request. |
||||
*/ |
||||
public class NetworkImageView extends ImageView { |
||||
/** The URL of the network image to load */ |
||||
private String mUrl; |
||||
|
||||
/** |
||||
* Resource ID of the image to be used as a placeholder until the network image is loaded. |
||||
*/ |
||||
private int mDefaultImageId; |
||||
|
||||
/** |
||||
* Resource ID of the image to be used if the network response fails. |
||||
*/ |
||||
private int mErrorImageId; |
||||
|
||||
/** Local copy of the ImageLoader. */ |
||||
private ImageLoader mImageLoader; |
||||
|
||||
/** Current ImageContainer. (either in-flight or finished) */ |
||||
private ImageContainer mImageContainer; |
||||
|
||||
public NetworkImageView(Context context) { |
||||
this(context, null); |
||||
} |
||||
|
||||
public NetworkImageView(Context context, AttributeSet attrs) { |
||||
this(context, attrs, 0); |
||||
} |
||||
|
||||
public NetworkImageView(Context context, AttributeSet attrs, int defStyle) { |
||||
super(context, attrs, defStyle); |
||||
} |
||||
|
||||
/** |
||||
* Sets URL of the image that should be loaded into this view. Note that calling this will |
||||
* immediately either set the cached image (if available) or the default image specified by |
||||
* {@link NetworkImageView#setDefaultImageResId(int)} on the view. |
||||
* |
||||
* NOTE: If applicable, {@link NetworkImageView#setDefaultImageResId(int)} and |
||||
* {@link NetworkImageView#setErrorImageResId(int)} should be called prior to calling |
||||
* this function. |
||||
* |
||||
* @param url The URL that should be loaded into this ImageView. |
||||
* @param imageLoader ImageLoader that will be used to make the request. |
||||
*/ |
||||
public void setImageUrl(String url, ImageLoader imageLoader) { |
||||
mUrl = url; |
||||
mImageLoader = imageLoader; |
||||
// The URL has potentially changed. See if we need to load it.
|
||||
loadImageIfNecessary(false); |
||||
} |
||||
|
||||
/** |
||||
* Sets the default image resource ID to be used for this view until the attempt to load it |
||||
* completes. |
||||
*/ |
||||
public void setDefaultImageResId(int defaultImage) { |
||||
mDefaultImageId = defaultImage; |
||||
} |
||||
|
||||
/** |
||||
* Sets the error image resource ID to be used for this view in the event that the image |
||||
* requested fails to load. |
||||
*/ |
||||
public void setErrorImageResId(int errorImage) { |
||||
mErrorImageId = errorImage; |
||||
} |
||||
|
||||
/** |
||||
* Loads the image for the view if it isn't already loaded. |
||||
* @param isInLayoutPass True if this was invoked from a layout pass, false otherwise. |
||||
*/ |
||||
void loadImageIfNecessary(final boolean isInLayoutPass) { |
||||
int width = getWidth(); |
||||
int height = getHeight(); |
||||
|
||||
boolean wrapWidth = false, wrapHeight = false; |
||||
if (getLayoutParams() != null) { |
||||
wrapWidth = getLayoutParams().width == LayoutParams.WRAP_CONTENT; |
||||
wrapHeight = getLayoutParams().height == LayoutParams.WRAP_CONTENT; |
||||
} |
||||
|
||||
// if the view's bounds aren't known yet, and this is not a wrap-content/wrap-content
|
||||
// view, hold off on loading the image.
|
||||
boolean isFullyWrapContent = wrapWidth && wrapHeight; |
||||
if (width == 0 && height == 0 && !isFullyWrapContent) { |
||||
return; |
||||
} |
||||
|
||||
// if the URL to be loaded in this view is empty, cancel any old requests and clear the
|
||||
// currently loaded image.
|
||||
if (TextUtils.isEmpty(mUrl)) { |
||||
if (mImageContainer != null) { |
||||
mImageContainer.cancelRequest(); |
||||
mImageContainer = null; |
||||
} |
||||
setDefaultImageOrNull(); |
||||
return; |
||||
} |
||||
|
||||
// if there was an old request in this view, check if it needs to be canceled.
|
||||
if (mImageContainer != null && mImageContainer.getRequestUrl() != null) { |
||||
if (mImageContainer.getRequestUrl().equals(mUrl)) { |
||||
// if the request is from the same URL, return.
|
||||
return; |
||||
} else { |
||||
// if there is a pre-existing request, cancel it if it's fetching a different URL.
|
||||
mImageContainer.cancelRequest(); |
||||
setDefaultImageOrNull(); |
||||
} |
||||
} |
||||
|
||||
// Calculate the max image width / height to use while ignoring WRAP_CONTENT dimens.
|
||||
int maxWidth = wrapWidth ? 0 : width; |
||||
int maxHeight = wrapHeight ? 0 : height; |
||||
|
||||
// The pre-existing content of this view didn't match the current URL. Load the new image
|
||||
// from the network.
|
||||
ImageContainer newContainer = mImageLoader.get(mUrl, |
||||
new ImageListener() { |
||||
@Override |
||||
public void onErrorResponse(VolleyError error) { |
||||
if (mErrorImageId != 0) { |
||||
setImageResource(mErrorImageId); |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public void onResponse(final ImageContainer response, boolean isImmediate) { |
||||
// If this was an immediate response that was delivered inside of a layout
|
||||
// pass do not set the image immediately as it will trigger a requestLayout
|
||||
// inside of a layout. Instead, defer setting the image by posting back to
|
||||
// the main thread.
|
||||
if (isImmediate && isInLayoutPass) { |
||||
post(new Runnable() { |
||||
@Override |
||||
public void run() { |
||||
onResponse(response, false); |
||||
} |
||||
}); |
||||
return; |
||||
} |
||||
|
||||
if (response.getBitmap() != null) { |
||||
setImageBitmap(response.getBitmap()); |
||||
} else if (mDefaultImageId != 0) { |
||||
setImageResource(mDefaultImageId); |
||||
} |
||||
} |
||||
}, maxWidth, maxHeight); |
||||
|
||||
// update the ImageContainer to be the new bitmap container.
|
||||
mImageContainer = newContainer; |
||||
} |
||||
|
||||
private void setDefaultImageOrNull() { |
||||
if(mDefaultImageId != 0) { |
||||
setImageResource(mDefaultImageId); |
||||
} |
||||
else { |
||||
setImageBitmap(null); |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
protected void onLayout(boolean changed, int left, int top, int right, int bottom) { |
||||
super.onLayout(changed, left, top, right, bottom); |
||||
loadImageIfNecessary(true); |
||||
} |
||||
|
||||
@Override |
||||
protected void onDetachedFromWindow() { |
||||
if (mImageContainer != null) { |
||||
// If the view was bound to an image request, cancel it and clear
|
||||
// out the image from the view.
|
||||
mImageContainer.cancelRequest(); |
||||
setImageBitmap(null); |
||||
// also clear out the container so we can reload the image if necessary.
|
||||
mImageContainer = null; |
||||
} |
||||
super.onDetachedFromWindow(); |
||||
} |
||||
|
||||
@Override |
||||
protected void drawableStateChanged() { |
||||
super.drawableStateChanged(); |
||||
invalidate(); |
||||
} |
||||
} |
@ -1,49 +0,0 @@ |
||||
/* |
||||
* Copyright (C) 2011 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.android.volley.toolbox; |
||||
|
||||
import com.android.volley.Cache; |
||||
|
||||
/** |
||||
* A cache that doesn't. |
||||
*/ |
||||
public class NoCache implements Cache { |
||||
@Override |
||||
public void clear() { |
||||
} |
||||
|
||||
@Override |
||||
public Entry get(String key) { |
||||
return null; |
||||
} |
||||
|
||||
@Override |
||||
public void put(String key, Entry entry) { |
||||
} |
||||
|
||||
@Override |
||||
public void invalidate(String key, boolean fullExpire) { |
||||
} |
||||
|
||||
@Override |
||||
public void remove(String key) { |
||||
} |
||||
|
||||
@Override |
||||
public void initialize() { |
||||
} |
||||
} |
@ -1,94 +0,0 @@ |
||||
/* |
||||
* Copyright (C) 2012 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.android.volley.toolbox; |
||||
|
||||
import java.io.ByteArrayOutputStream; |
||||
import java.io.IOException; |
||||
|
||||
/** |
||||
* A variation of {@link java.io.ByteArrayOutputStream} that uses a pool of byte[] buffers instead |
||||
* of always allocating them fresh, saving on heap churn. |
||||
*/ |
||||
public class PoolingByteArrayOutputStream extends ByteArrayOutputStream { |
||||
/** |
||||
* If the {@link #PoolingByteArrayOutputStream(ByteArrayPool)} constructor is called, this is |
||||
* the default size to which the underlying byte array is initialized. |
||||
*/ |
||||
private static final int DEFAULT_SIZE = 256; |
||||
|
||||
private final ByteArrayPool mPool; |
||||
|
||||
/** |
||||
* Constructs a new PoolingByteArrayOutputStream with a default size. If more bytes are written |
||||
* to this instance, the underlying byte array will expand. |
||||
*/ |
||||
public PoolingByteArrayOutputStream(ByteArrayPool pool) { |
||||
this(pool, DEFAULT_SIZE); |
||||
} |
||||
|
||||
/** |
||||
* Constructs a new {@code ByteArrayOutputStream} with a default size of {@code size} bytes. If |
||||
* more than {@code size} bytes are written to this instance, the underlying byte array will |
||||
* expand. |
||||
* |
||||
* @param size initial size for the underlying byte array. The value will be pinned to a default |
||||
* minimum size. |
||||
*/ |
||||
public PoolingByteArrayOutputStream(ByteArrayPool pool, int size) { |
||||
mPool = pool; |
||||
buf = mPool.getBuf(Math.max(size, DEFAULT_SIZE)); |
||||
} |
||||
|
||||
@Override |
||||
public void close() throws IOException { |
||||
mPool.returnBuf(buf); |
||||
buf = null; |
||||
super.close(); |
||||
} |
||||
|
||||
@Override |
||||
public void finalize() throws Throwable { |
||||
mPool.returnBuf(buf); |
||||
super.finalize(); |
||||
} |
||||
|
||||
/** |
||||
* Ensures there is enough space in the buffer for the given number of additional bytes. |
||||
*/ |
||||
private void expand(int i) { |
||||
/* Can the buffer handle @i more bytes, if not expand it */ |
||||
if (count + i <= buf.length) { |
||||
return; |
||||
} |
||||
byte[] newbuf = mPool.getBuf((count + i) * 2); |
||||
System.arraycopy(buf, 0, newbuf, 0, count); |
||||
mPool.returnBuf(buf); |
||||
buf = newbuf; |
||||
} |
||||
|
||||
@Override |
||||
public synchronized void write(byte[] buffer, int offset, int len) { |
||||
expand(len); |
||||
super.write(buffer, offset, len); |
||||
} |
||||
|
||||
@Override |
||||
public synchronized void write(int oneByte) { |
||||
expand(1); |
||||
super.write(oneByte); |
||||
} |
||||
} |
@ -1,153 +0,0 @@ |
||||
/* |
||||
* Copyright (C) 2011 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.android.volley.toolbox; |
||||
|
||||
import com.android.volley.Request; |
||||
import com.android.volley.Response; |
||||
import com.android.volley.VolleyError; |
||||
|
||||
import java.util.concurrent.ExecutionException; |
||||
import java.util.concurrent.Future; |
||||
import java.util.concurrent.TimeUnit; |
||||
import java.util.concurrent.TimeoutException; |
||||
|
||||
/** |
||||
* A Future that represents a Volley request. |
||||
* |
||||
* Used by providing as your response and error listeners. For example: |
||||
* <pre> |
||||
* RequestFuture<JSONObject> future = RequestFuture.newFuture(); |
||||
* MyRequest request = new MyRequest(URL, future, future); |
||||
* |
||||
* // If you want to be able to cancel the request:
|
||||
* future.setRequest(requestQueue.add(request)); |
||||
* |
||||
* // Otherwise:
|
||||
* requestQueue.add(request); |
||||
* |
||||
* try { |
||||
* JSONObject response = future.get(); |
||||
* // do something with response
|
||||
* } catch (InterruptedException e) { |
||||
* // handle the error
|
||||
* } catch (ExecutionException e) { |
||||
* // handle the error
|
||||
* } |
||||
* </pre> |
||||
* |
||||
* @param <T> The type of parsed response this future expects. |
||||
*/ |
||||
public class RequestFuture<T> implements Future<T>, Response.Listener<T>, |
||||
Response.ErrorListener { |
||||
private Request<?> mRequest; |
||||
private boolean mResultReceived = false; |
||||
private T mResult; |
||||
private VolleyError mException; |
||||
|
||||
public static <E> RequestFuture<E> newFuture() { |
||||
return new RequestFuture<E>(); |
||||
} |
||||
|
||||
private RequestFuture() {} |
||||
|
||||
public void setRequest(Request<?> request) { |
||||
mRequest = request; |
||||
} |
||||
|
||||
@Override |
||||
public synchronized boolean cancel(boolean mayInterruptIfRunning) { |
||||
if (mRequest == null) { |
||||
return false; |
||||
} |
||||
|
||||
if (!isDone()) { |
||||
mRequest.cancel(); |
||||
return true; |
||||
} else { |
||||
return false; |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public T get() throws InterruptedException, ExecutionException { |
||||
try { |
||||
return doGet(null); |
||||
} catch (TimeoutException e) { |
||||
throw new AssertionError(e); |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public T get(long timeout, TimeUnit unit) |
||||
throws InterruptedException, ExecutionException, TimeoutException { |
||||
return doGet(TimeUnit.MILLISECONDS.convert(timeout, unit)); |
||||
} |
||||
|
||||
private synchronized T doGet(Long timeoutMs) |
||||
throws InterruptedException, ExecutionException, TimeoutException { |
||||
if (mException != null) { |
||||
throw new ExecutionException(mException); |
||||
} |
||||
|
||||
if (mResultReceived) { |
||||
return mResult; |
||||
} |
||||
|
||||
if (timeoutMs == null) { |
||||
wait(0); |
||||
} else if (timeoutMs > 0) { |
||||
wait(timeoutMs); |
||||
} |
||||
|
||||
if (mException != null) { |
||||
throw new ExecutionException(mException); |
||||
} |
||||
|
||||
if (!mResultReceived) { |
||||
throw new TimeoutException(); |
||||
} |
||||
|
||||
return mResult; |
||||
} |
||||
|
||||
@Override |
||||
public boolean isCancelled() { |
||||
if (mRequest == null) { |
||||
return false; |
||||
} |
||||
return mRequest.isCanceled(); |
||||
} |
||||
|
||||
@Override |
||||
public synchronized boolean isDone() { |
||||
return mResultReceived || mException != null || isCancelled(); |
||||
} |
||||
|
||||
@Override |
||||
public synchronized void onResponse(T response) { |
||||
mResultReceived = true; |
||||
mResult = response; |
||||
notifyAll(); |
||||
} |
||||
|
||||
@Override |
||||
public synchronized void onErrorResponse(VolleyError error) { |
||||
mException = error; |
||||
notifyAll(); |
||||
} |
||||
} |
||||
|
@ -1,73 +0,0 @@ |
||||
/* |
||||
* Copyright (C) 2011 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.android.volley.toolbox; |
||||
|
||||
import com.android.volley.NetworkResponse; |
||||
import com.android.volley.Request; |
||||
import com.android.volley.Response; |
||||
import com.android.volley.Response.ErrorListener; |
||||
import com.android.volley.Response.Listener; |
||||
|
||||
import java.io.UnsupportedEncodingException; |
||||
|
||||
/** |
||||
* A canned request for retrieving the response body at a given URL as a String. |
||||
*/ |
||||
public class StringRequest extends Request<String> { |
||||
private final Listener<String> mListener; |
||||
|
||||
/** |
||||
* Creates a new request with the given method. |
||||
* |
||||
* @param method the request {@link Method} to use |
||||
* @param url URL to fetch the string at |
||||
* @param listener Listener to receive the String response |
||||
* @param errorListener Error listener, or null to ignore errors |
||||
*/ |
||||
public StringRequest(int method, String url, Listener<String> listener, |
||||
ErrorListener errorListener) { |
||||
super(method, url, errorListener); |
||||
mListener = listener; |
||||
} |
||||
|
||||
/** |
||||
* Creates a new GET request. |
||||
* |
||||
* @param url URL to fetch the string at |
||||
* @param listener Listener to receive the String response |
||||
* @param errorListener Error listener, or null to ignore errors |
||||
*/ |
||||
public StringRequest(String url, Listener<String> listener, ErrorListener errorListener) { |
||||
this(Method.GET, url, listener, errorListener); |
||||
} |
||||
|
||||
@Override |
||||
protected void deliverResponse(String response) { |
||||
mListener.onResponse(response); |
||||
} |
||||
|
||||
@Override |
||||
protected Response<String> parseNetworkResponse(NetworkResponse response) { |
||||
String parsed; |
||||
try { |
||||
parsed = new String(response.data, HttpHeaderParser.parseCharset(response.headers)); |
||||
} catch (UnsupportedEncodingException e) { |
||||
parsed = new String(response.data); |
||||
} |
||||
return Response.success(parsed, HttpHeaderParser.parseCacheHeaders(response)); |
||||
} |
||||
} |
@ -1,58 +0,0 @@ |
||||
/* |
||||
* Copyright (C) 2012 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.android.volley.toolbox; |
||||
|
||||
import android.content.Context; |
||||
import android.net.http.AndroidHttpClient; |
||||
import android.os.Build; |
||||
|
||||
import com.android.volley.Network; |
||||
import com.android.volley.RequestQueue; |
||||
import com.android.volley.compat.NoSSLv3Compat; |
||||
|
||||
import java.io.File; |
||||
|
||||
public class Volley { |
||||
|
||||
/** Default on-disk cache directory. */ |
||||
public static final String DEFAULT_CACHE_DIR = "volley"; |
||||
|
||||
public static RequestQueue newRequestQueue(Context context, String userAgent, HttpStack stack, File cacheDir, int diskCacheSize) { |
||||
if (stack == null) { |
||||
if (Build.VERSION.SDK_INT >= 9) { |
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { |
||||
// Use a socket factory that removes sslv3
|
||||
stack = new HurlStack(userAgent, null, new NoSSLv3Compat.NoSSLv3Factory()); |
||||
} else { |
||||
stack = new HurlStack(userAgent); |
||||
} |
||||
} else { |
||||
// Prior to Gingerbread, HttpUrlConnection was unreliable.
|
||||
// See: http://android-developers.blogspot.com/2011/09/androids-http-clients.html
|
||||
stack = new HttpClientStack(AndroidHttpClient.newInstance(userAgent)); |
||||
} |
||||
} |
||||
|
||||
Network network = new BasicNetwork(stack); |
||||
|
||||
DiskBasedCache diskCache = diskCacheSize < 0 ? new DiskBasedCache(cacheDir) : new DiskBasedCache(cacheDir, diskCacheSize); |
||||
RequestQueue queue = new RequestQueue(diskCache, network); |
||||
queue.start(); |
||||
|
||||
return queue; |
||||
} |
||||
} |
@ -0,0 +1,16 @@ |
||||
package org.floens.chan.ui.notification; |
||||
|
||||
import android.app.NotificationManager; |
||||
import android.content.Context; |
||||
|
||||
public class NotificationHelper { |
||||
protected final Context applicationContext; |
||||
protected final NotificationManager notificationManager; |
||||
|
||||
public NotificationHelper(Context applicationContext) { |
||||
this.applicationContext = applicationContext; |
||||
|
||||
notificationManager = (NotificationManager) applicationContext. |
||||
getSystemService(Context.NOTIFICATION_SERVICE); |
||||
} |
||||
} |
@ -0,0 +1,195 @@ |
||||
package org.floens.chan.ui.notification; |
||||
|
||||
import android.annotation.TargetApi; |
||||
import android.app.NotificationChannel; |
||||
import android.app.NotificationManager; |
||||
import android.app.PendingIntent; |
||||
import android.content.Context; |
||||
import android.content.Intent; |
||||
import android.os.Build; |
||||
import android.support.v4.app.NotificationCompat; |
||||
|
||||
import org.floens.chan.R; |
||||
import org.floens.chan.core.manager.WatchManager; |
||||
import org.floens.chan.core.model.Post; |
||||
import org.floens.chan.core.model.orm.Pin; |
||||
import org.floens.chan.ui.activity.BoardActivity; |
||||
|
||||
import java.util.List; |
||||
import java.util.regex.Pattern; |
||||
|
||||
import javax.inject.Inject; |
||||
import javax.inject.Singleton; |
||||
|
||||
@Singleton |
||||
public class ThreadWatchNotifications extends NotificationHelper { |
||||
public static final String CHANNEL_ID_WATCH_NORMAL = "watch:normal"; |
||||
public static final String CHANNEL_ID_WATCH_MENTION = "watch:mention"; |
||||
private static final int NOTIFICATION_ID_WATCH_NORMAL = 0x10000; |
||||
private static final int NOTIFICATION_ID_WATCH_NORMAL_MASK = 0xffff; |
||||
private static final int NOTIFICATION_ID_WATCH_MENTION = 0x20000; |
||||
private static final int NOTIFICATION_ID_WATCH_MENTION_MASK = 0xffff; |
||||
|
||||
private static final String POST_COMMENT_IMAGE_PREFIX = "(img) "; |
||||
private static final Pattern POST_COMMENT_SHORTEN_NO_PATTERN = |
||||
Pattern.compile(">>\\d+(?=\\d{4})(\\d{4})"); |
||||
|
||||
private int pendingIntentCounter = 0; |
||||
|
||||
@Inject |
||||
public ThreadWatchNotifications(Context applicationContext) { |
||||
super(applicationContext); |
||||
} |
||||
|
||||
public void showForWatchers(List<WatchManager.PinWatcher> pinWatchers) { |
||||
showPinSummaries(pinWatchers); |
||||
} |
||||
|
||||
public void hideAll() { |
||||
notificationManager.cancelAll(); |
||||
} |
||||
|
||||
@TargetApi(Build.VERSION_CODES.O) |
||||
private void showPinSummaries(List<WatchManager.PinWatcher> pinWatchers) { |
||||
if (isOreo()) { |
||||
ensureChannels(); |
||||
} |
||||
|
||||
for (WatchManager.PinWatcher pinWatcher : pinWatchers) { |
||||
if (!pinWatcher.requiresNotificationUpdate()) { |
||||
continue; |
||||
} |
||||
|
||||
// Normal thread posts.
|
||||
int normalId = NOTIFICATION_ID_WATCH_NORMAL + |
||||
(pinWatcher.getPinId() & NOTIFICATION_ID_WATCH_NORMAL_MASK); |
||||
if (!pinWatcher.getUnviewedPosts().isEmpty()) { |
||||
NotificationCompat.Builder builder = |
||||
buildMessagingStyleNotification(pinWatcher, pinWatcher.getUnviewedPosts(), |
||||
false, CHANNEL_ID_WATCH_NORMAL); |
||||
notificationManager.notify(normalId, builder.build()); |
||||
} else { |
||||
notificationManager.cancel(normalId); |
||||
} |
||||
|
||||
// Posts that mention you.
|
||||
int mentionId = NOTIFICATION_ID_WATCH_MENTION + |
||||
(pinWatcher.getPinId() & NOTIFICATION_ID_WATCH_MENTION_MASK); |
||||
if (!pinWatcher.getUnviewedQuotes().isEmpty()) { |
||||
NotificationCompat.Builder builder = |
||||
buildMessagingStyleNotification(pinWatcher, pinWatcher.getUnviewedQuotes(), |
||||
true, CHANNEL_ID_WATCH_MENTION); |
||||
|
||||
notificationManager.notify(mentionId, builder.build()); |
||||
} else { |
||||
notificationManager.cancel(mentionId); |
||||
} |
||||
|
||||
pinWatcher.hadNotificationUpdate(); |
||||
} |
||||
} |
||||
|
||||
private NotificationCompat.Builder buildMessagingStyleNotification( |
||||
WatchManager.PinWatcher pinWatcher, List<Post> posts, boolean mentions, |
||||
String channelId) { |
||||
NotificationCompat.Builder builder = |
||||
new NotificationCompat.Builder(applicationContext, channelId); |
||||
|
||||
NotificationCompat.MessagingStyle messagingStyle = |
||||
new NotificationCompat.MessagingStyle(""); |
||||
|
||||
builder.setSmallIcon(!mentions ? |
||||
R.drawable.ic_stat_notify : R.drawable.ic_stat_notify_alert); |
||||
if (mentions) { |
||||
builder.setSubText("Mentions"); |
||||
} |
||||
if (pinWatcher.getThumbnailBitmap() != null) { |
||||
builder.setLargeIcon(pinWatcher.getThumbnailBitmap()); |
||||
} |
||||
if (mentions && !isOreo()) { |
||||
builder.setDefaults(NotificationCompat.DEFAULT_SOUND | |
||||
NotificationCompat.DEFAULT_VIBRATE); |
||||
} |
||||
|
||||
String subTitle; |
||||
if (!mentions) { |
||||
subTitle = "(" + posts.size() + ")"; |
||||
} else { |
||||
subTitle = "(" + posts.size() + " mentions)"; |
||||
} |
||||
messagingStyle.setConversationTitle(pinWatcher.getTitle() + " " + subTitle); |
||||
messagingStyle.setGroupConversation(true); |
||||
addPostsToMessagingStyle(messagingStyle, posts); |
||||
builder.setStyle(messagingStyle); |
||||
|
||||
setNotificationIntent(builder, pinWatcher.getPinId()); |
||||
|
||||
return builder; |
||||
} |
||||
|
||||
private void addPostsToMessagingStyle(NotificationCompat.MessagingStyle messagingStyle, |
||||
List<Post> unviewedPosts) { |
||||
final int maxLines = 25; |
||||
|
||||
if (unviewedPosts.size() > maxLines) { |
||||
unviewedPosts = unviewedPosts.subList( |
||||
unviewedPosts.size() - maxLines, unviewedPosts.size()); |
||||
} |
||||
|
||||
for (Post post : unviewedPosts) { |
||||
String comment = post.image() != null ? POST_COMMENT_IMAGE_PREFIX : ""; |
||||
if (post.comment.length() > 0) { |
||||
comment += post.comment; |
||||
} |
||||
|
||||
// Replace >>132456798 with >6789 to shorten the notification
|
||||
comment = POST_COMMENT_SHORTEN_NO_PATTERN.matcher(comment) |
||||
.replaceAll(">$1"); |
||||
|
||||
CharSequence name = post.nameTripcodeIdCapcodeSpan; |
||||
// if (name.length() == 0) {
|
||||
// name = "Anonymous";
|
||||
// }
|
||||
messagingStyle.addMessage(comment, post.time, name); |
||||
} |
||||
} |
||||
|
||||
private void setNotificationIntent(NotificationCompat.Builder builder, int pinId) { |
||||
Intent intent = new Intent(applicationContext, BoardActivity.class); |
||||
intent.setAction(Intent.ACTION_MAIN); |
||||
intent.addCategory(Intent.CATEGORY_LAUNCHER); |
||||
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | |
||||
Intent.FLAG_ACTIVITY_SINGLE_TOP | |
||||
Intent.FLAG_ACTIVITY_NEW_TASK | |
||||
Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); |
||||
|
||||
intent.putExtra("pin_id", pinId); |
||||
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity( |
||||
applicationContext, ++pendingIntentCounter, |
||||
intent, PendingIntent.FLAG_UPDATE_CURRENT); |
||||
|
||||
builder.setContentIntent(pendingIntent); |
||||
} |
||||
|
||||
@TargetApi(Build.VERSION_CODES.O) |
||||
public void ensureChannels() { |
||||
NotificationChannel normalChannel = new NotificationChannel( |
||||
CHANNEL_ID_WATCH_NORMAL, "Thread updates", |
||||
NotificationManager.IMPORTANCE_DEFAULT); |
||||
normalChannel.setDescription("Normal posts for threads"); |
||||
notificationManager.createNotificationChannel(normalChannel); |
||||
|
||||
NotificationChannel mentionChannel = new NotificationChannel( |
||||
CHANNEL_ID_WATCH_MENTION, "Thread mentions", |
||||
NotificationManager.IMPORTANCE_HIGH); |
||||
mentionChannel.setDescription("Posts were you were mentioned"); |
||||
mentionChannel.enableVibration(true); |
||||
mentionChannel.enableLights(true); |
||||
notificationManager.createNotificationChannel(mentionChannel); |
||||
} |
||||
|
||||
private boolean isOreo() { |
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O; |
||||
} |
||||
} |
@ -1,304 +0,0 @@ |
||||
/* |
||||
* Clover - 4chan browser https://github.com/Floens/Clover/
|
||||
* Copyright (C) 2014 Floens |
||||
* |
||||
* This program is free software: you can redistribute it and/or modify |
||||
* it under the terms of the GNU General Public License as published by |
||||
* the Free Software Foundation, either version 3 of the License, or |
||||
* (at your option) any later version. |
||||
* |
||||
* This program is distributed in the hope that it will be useful, |
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
* GNU General Public License for more details. |
||||
* |
||||
* You should have received a copy of the GNU General Public License |
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/ |
||||
package org.floens.chan.ui.service; |
||||
|
||||
import android.app.Notification; |
||||
import android.app.NotificationManager; |
||||
import android.app.PendingIntent; |
||||
import android.app.Service; |
||||
import android.content.Context; |
||||
import android.content.Intent; |
||||
import android.os.IBinder; |
||||
import android.support.v4.app.NotificationCompat; |
||||
import android.text.TextUtils; |
||||
|
||||
import org.floens.chan.Chan; |
||||
import org.floens.chan.R; |
||||
import org.floens.chan.core.manager.WatchManager; |
||||
import org.floens.chan.core.model.Post; |
||||
import org.floens.chan.core.model.orm.Pin; |
||||
import org.floens.chan.core.settings.ChanSettings; |
||||
import org.floens.chan.ui.activity.BoardActivity; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.Collections; |
||||
import java.util.Comparator; |
||||
import java.util.List; |
||||
import java.util.regex.Pattern; |
||||
|
||||
import javax.inject.Inject; |
||||
|
||||
import static org.floens.chan.Chan.inject; |
||||
|
||||
public class WatchNotifier extends Service { |
||||
private static final String TAG = "WatchNotifier"; |
||||
private static final int NOTIFICATION_ID = 1; |
||||
private static final PostAgeComparator POST_AGE_COMPARATOR = new PostAgeComparator(); |
||||
private static final int SUBJECT_LENGTH = 6; |
||||
private static final String IMAGE_TEXT = "(img) "; |
||||
private static final Pattern SHORTEN_NO_PATTERN = Pattern.compile(">>\\d+(?=\\d{3})(\\d{3})"); |
||||
|
||||
private NotificationManager nm; |
||||
|
||||
@Inject |
||||
WatchManager watchManager; |
||||
|
||||
@Override |
||||
public IBinder onBind(final Intent intent) { |
||||
return null; |
||||
} |
||||
|
||||
@Override |
||||
public void onCreate() { |
||||
super.onCreate(); |
||||
inject(this); |
||||
|
||||
nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); |
||||
|
||||
startForeground(NOTIFICATION_ID, createNotification()); |
||||
} |
||||
|
||||
@Override |
||||
public void onDestroy() { |
||||
super.onDestroy(); |
||||
nm.cancel(NOTIFICATION_ID); |
||||
} |
||||
|
||||
@Override |
||||
public int onStartCommand(Intent intent, int flags, int startId) { |
||||
if (intent != null && intent.getExtras() != null && intent.getExtras().getBoolean("pause_pins", false)) { |
||||
pausePins(); |
||||
} else { |
||||
updateNotification(); |
||||
} |
||||
|
||||
return START_STICKY; |
||||
} |
||||
|
||||
public void updateNotification() { |
||||
nm.notify(NOTIFICATION_ID, createNotification()); |
||||
} |
||||
|
||||
public void pausePins() { |
||||
watchManager.pauseAll(); |
||||
} |
||||
|
||||
private Notification createNotification() { |
||||
boolean notifyQuotesOnly = ChanSettings.watchNotifyMode.get().equals("quotes"); |
||||
boolean soundQuotesOnly = ChanSettings.watchSound.get().equals("quotes"); |
||||
|
||||
List<Post> unviewedPosts = new ArrayList<>(); |
||||
List<Post> listQuoting = new ArrayList<>(); |
||||
List<Pin> pins = new ArrayList<>(); |
||||
List<Pin> subjectPins = new ArrayList<>(); |
||||
|
||||
boolean light = false; |
||||
boolean sound = false; |
||||
boolean peek = false; |
||||
|
||||
for (Pin pin : watchManager.getWatchingPins()) { |
||||
WatchManager.PinWatcher watcher = watchManager.getPinWatcher(pin); |
||||
if (watcher == null || pin.isError) { |
||||
continue; |
||||
} |
||||
|
||||
pins.add(pin); |
||||
|
||||
if (notifyQuotesOnly) { |
||||
unviewedPosts.addAll(watcher.getUnviewedQuotes()); |
||||
listQuoting.addAll(watcher.getUnviewedQuotes()); |
||||
if (watcher.getWereNewQuotes()) { |
||||
light = true; |
||||
sound = true; |
||||
peek = true; |
||||
} |
||||
if (pin.getNewQuoteCount() > 0) { |
||||
subjectPins.add(pin); |
||||
} |
||||
} else { |
||||
unviewedPosts.addAll(watcher.getUnviewedPosts()); |
||||
listQuoting.addAll(watcher.getUnviewedQuotes()); |
||||
if (watcher.getWereNewPosts()) { |
||||
light = true; |
||||
if (!soundQuotesOnly) { |
||||
sound = true; |
||||
peek = true; |
||||
} |
||||
} |
||||
if (watcher.getWereNewQuotes()) { |
||||
sound = true; |
||||
peek = true; |
||||
} |
||||
if (pin.getNewPostCount() > 0) { |
||||
subjectPins.add(pin); |
||||
} |
||||
} |
||||
} |
||||
|
||||
if (Chan.getInstance().getApplicationInForeground()) { |
||||
light = false; |
||||
sound = false; |
||||
} |
||||
|
||||
if (!ChanSettings.watchPeek.get()) { |
||||
peek = false; |
||||
} |
||||
|
||||
return notifyAboutPosts(pins, subjectPins, unviewedPosts, listQuoting, notifyQuotesOnly, light, sound, peek); |
||||
} |
||||
|
||||
private Notification notifyAboutPosts(List<Pin> pins, List<Pin> subjectPins, List<Post> unviewedPosts, List<Post> listQuoting, |
||||
boolean notifyQuotesOnly, boolean light, boolean sound, boolean peek) { |
||||
String title = getResources().getQuantityString(R.plurals.watch_title, pins.size(), pins.size()); |
||||
|
||||
if (unviewedPosts.isEmpty()) { |
||||
// Idle notification
|
||||
String message = getString(R.string.watch_idle); |
||||
return get(title, message, null, false, false, false, false, null); |
||||
} else { |
||||
// New posts notification
|
||||
String message; |
||||
List<Post> postsForExpandedLines; |
||||
if (notifyQuotesOnly) { |
||||
message = getResources().getQuantityString(R.plurals.watch_new_quotes, listQuoting.size(), listQuoting.size()); |
||||
postsForExpandedLines = listQuoting; |
||||
} else { |
||||
postsForExpandedLines = unviewedPosts; |
||||
if (listQuoting.size() > 0) { |
||||
message = getResources().getQuantityString(R.plurals.watch_new_quoting, unviewedPosts.size(), unviewedPosts.size(), listQuoting.size()); |
||||
} else { |
||||
message = getResources().getQuantityString(R.plurals.watch_new, unviewedPosts.size(), unviewedPosts.size()); |
||||
} |
||||
} |
||||
|
||||
Collections.sort(postsForExpandedLines, POST_AGE_COMPARATOR); |
||||
List<CharSequence> expandedLines = new ArrayList<>(); |
||||
for (Post postForExpandedLine : postsForExpandedLines) { |
||||
CharSequence prefix; |
||||
if (postForExpandedLine.getTitle().length() <= SUBJECT_LENGTH) { |
||||
prefix = postForExpandedLine.getTitle(); |
||||
} else { |
||||
prefix = postForExpandedLine.getTitle().subSequence(0, SUBJECT_LENGTH); |
||||
} |
||||
|
||||
String comment = postForExpandedLine.image() != null ? IMAGE_TEXT : ""; |
||||
if (postForExpandedLine.comment.length() > 0) { |
||||
comment += postForExpandedLine.comment; |
||||
} |
||||
|
||||
// Replace >>132456798 with >789 to shorten the notification
|
||||
comment = SHORTEN_NO_PATTERN.matcher(comment).replaceAll(">$1"); |
||||
|
||||
expandedLines.add(prefix + ": " + comment); |
||||
} |
||||
|
||||
Pin targetPin = null; |
||||
if (subjectPins.size() == 1) { |
||||
targetPin = subjectPins.get(0); |
||||
} |
||||
|
||||
String smallText = TextUtils.join(", ", expandedLines); |
||||
return get(message, smallText, expandedLines, light, sound, peek, true, targetPin); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Create a notification with the supplied parameters. |
||||
* The style of the big notification is InboxStyle, a list of text. |
||||
* |
||||
* @param title The title of the notification |
||||
* @param smallText The content of the small notification |
||||
* @param expandedLines A list of lines for the big notification, or null if not shown |
||||
* @param sound Should the notification make a sound |
||||
* @param peek Peek the notification into the screen |
||||
* @param alertIcon Show the alert version of the icon |
||||
* @param target The target pin, or null to open the pinned pane on tap |
||||
*/ |
||||
private Notification get(String title, String smallText, List<CharSequence> expandedLines, |
||||
boolean light, boolean sound, boolean peek, boolean alertIcon, Pin target) { |
||||
Intent intent = new Intent(this, BoardActivity.class); |
||||
intent.setAction(Intent.ACTION_MAIN); |
||||
intent.addCategory(Intent.CATEGORY_LAUNCHER); |
||||
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_NEW_TASK | |
||||
Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); |
||||
|
||||
intent.putExtra("pin_id", target == null ? -1 : target.id); |
||||
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); |
||||
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(this); |
||||
if (sound || peek) { |
||||
builder.setDefaults(Notification.DEFAULT_SOUND | Notification.DEFAULT_VIBRATE); |
||||
} |
||||
|
||||
if (light) { |
||||
long watchLed = Long.parseLong(ChanSettings.watchLed.get(), 16); |
||||
if (watchLed >= 0) { |
||||
builder.setLights((int) watchLed, 1000, 1000); |
||||
} |
||||
} |
||||
|
||||
builder.setContentIntent(pendingIntent); |
||||
|
||||
builder.setContentTitle(title); |
||||
if (smallText != null) { |
||||
builder.setContentText(smallText); |
||||
} |
||||
|
||||
if (alertIcon || peek) { |
||||
builder.setSmallIcon(R.drawable.ic_stat_notify_alert); |
||||
builder.setPriority(NotificationCompat.PRIORITY_HIGH); |
||||
} else { |
||||
builder.setSmallIcon(R.drawable.ic_stat_notify); |
||||
builder.setPriority(NotificationCompat.PRIORITY_MIN); |
||||
} |
||||
|
||||
Intent pauseWatching = new Intent(this, WatchNotifier.class); |
||||
pauseWatching.putExtra("pause_pins", true); |
||||
|
||||
PendingIntent pauseWatchingPending = PendingIntent.getService(this, 0, pauseWatching, |
||||
PendingIntent.FLAG_UPDATE_CURRENT); |
||||
|
||||
builder.addAction(R.drawable.ic_action_pause, getString(R.string.watch_pause_pins), |
||||
pauseWatchingPending); |
||||
|
||||
if (expandedLines != null) { |
||||
NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle(); |
||||
for (CharSequence line : expandedLines.subList(Math.max(0, expandedLines.size() - 10), expandedLines.size())) { |
||||
style.addLine(line); |
||||
} |
||||
style.setBigContentTitle(title); |
||||
builder.setStyle(style); |
||||
} |
||||
|
||||
return builder.build(); |
||||
} |
||||
|
||||
private static class PostAgeComparator implements Comparator<Post> { |
||||
@Override |
||||
public int compare(Post lhs, Post rhs) { |
||||
if (lhs.time < rhs.time) { |
||||
return 1; |
||||
} else if (lhs.time > rhs.time) { |
||||
return -1; |
||||
} else { |
||||
return 0; |
||||
} |
||||
} |
||||
} |
||||
} |
@ -1,6 +1,6 @@ |
||||
#Sat Oct 06 14:30:14 CEST 2018 |
||||
#Sun Apr 21 13:06:50 CEST 2019 |
||||
distributionBase=GRADLE_USER_HOME |
||||
distributionPath=wrapper/dists |
||||
zipStoreBase=GRADLE_USER_HOME |
||||
zipStorePath=wrapper/dists |
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip |
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip |
||||
|
Loading…
Reference in new issue