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 |
distributionBase=GRADLE_USER_HOME |
||||||
distributionPath=wrapper/dists |
distributionPath=wrapper/dists |
||||||
zipStoreBase=GRADLE_USER_HOME |
zipStoreBase=GRADLE_USER_HOME |
||||||
zipStorePath=wrapper/dists |
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