diff --git a/settings.gradle b/settings.gradle index f5fac105..95c2cae7 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,4 +1,5 @@ include ':stetho' +include ':stetho-volley' include ':stetho-urlconnection' include ':stetho-okhttp' include ':stetho-sample' diff --git a/stetho-volley/.gitignore b/stetho-volley/.gitignore new file mode 100644 index 00000000..796b96d1 --- /dev/null +++ b/stetho-volley/.gitignore @@ -0,0 +1 @@ +/build diff --git a/stetho-volley/build.gradle b/stetho-volley/build.gradle new file mode 100644 index 00000000..ba5069ad --- /dev/null +++ b/stetho-volley/build.gradle @@ -0,0 +1,27 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion 21 + buildToolsVersion "21.1.2" + + defaultConfig { + minSdkVersion 9 + targetSdkVersion 21 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } + + lintOptions { + // This seems to be firing due to okio referencing java.nio.File + // which is harmless for us. Not sure how to disable this in + // more targeted fashion... + warning 'InvalidPackage' + } +} + +dependencies { + compile project(':stetho') + //provided scope, you must provide the volley jar in your project + provided files('libs/volley.jar') +} diff --git a/stetho-volley/libs/volley.jar b/stetho-volley/libs/volley.jar new file mode 100644 index 00000000..b881e79f Binary files /dev/null and b/stetho-volley/libs/volley.jar differ diff --git a/stetho-volley/src/androidTest/java/com/facebook/stetho/ApplicationTest.java b/stetho-volley/src/androidTest/java/com/facebook/stetho/ApplicationTest.java new file mode 100644 index 00000000..a96fd113 --- /dev/null +++ b/stetho-volley/src/androidTest/java/com/facebook/stetho/ApplicationTest.java @@ -0,0 +1,13 @@ +package com.facebook.stetho; + +import android.app.Application; +import android.test.ApplicationTestCase; + +/** + * Testing Fundamentals + */ +public class ApplicationTest extends ApplicationTestCase { + public ApplicationTest() { + super(Application.class); + } +} \ No newline at end of file diff --git a/stetho-volley/src/main/AndroidManifest.xml b/stetho-volley/src/main/AndroidManifest.xml new file mode 100644 index 00000000..83ce49bc --- /dev/null +++ b/stetho-volley/src/main/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + + + + diff --git a/stetho-volley/src/main/java/com/facebook/stetho/volley/StethoNetwork.java b/stetho-volley/src/main/java/com/facebook/stetho/volley/StethoNetwork.java new file mode 100644 index 00000000..2a9eebe5 --- /dev/null +++ b/stetho-volley/src/main/java/com/facebook/stetho/volley/StethoNetwork.java @@ -0,0 +1,309 @@ +/* + * 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. + */ +/* + * AHD Notes June 28, 2013: + * + * This file is essentially a copy of BasicNetwork + * ( https://android.googlesource.com/platform/frameworks/volley/+/cd8ce543d0a51dac9f6308cd8730816f690ead2f/src/com/android/volley/toolbox/BasicNetwork.java ) + * The only difference is that we need to accept as good more than just 200 and 204 response codes. + */ +package com.facebook.stetho.volley; + +import android.os.SystemClock; + +import com.android.volley.AuthFailureError; +import com.android.volley.Cache; +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 com.android.volley.toolbox.ByteArrayPool; +import com.android.volley.toolbox.HttpStack; +import com.android.volley.toolbox.PoolingByteArrayOutputStream; +import com.facebook.stetho.inspector.network.DefaultResponseHandler; +import com.facebook.stetho.inspector.network.NetworkEventReporter; +import com.facebook.stetho.inspector.network.NetworkEventReporterImpl; + +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.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * A network performing Volley requests over an {@link HttpStack}. Will track requests to {@link com.facebook.stetho.inspector.network.NetworkEventReporter} + * if it is enabled. Should be used when the {@link com.android.volley.RequestQueue} is created. For example: + *

+ *

+ * Network network = new LenientStatusCodeBasicNetwork(stack); + * RequestQueue queue = new RequestQueue(new DiskBasedCache(cacheDir), network); + * queue.start(); + */ +public class StethoNetwork implements Network { + + private final NetworkEventReporter mStethoHook = NetworkEventReporterImpl.get(); + private static final AtomicInteger sSequenceNumberGenerator = new AtomicInteger(0); + + 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 StethoNetwork(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 StethoNetwork(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 responseHeaders = new HashMap(); + try { + // Gather headers. + Map headers = new HashMap(); + addCacheHeaders(headers, request.getCacheEntry()); + + //do stetho things + int requestId = sSequenceNumberGenerator.incrementAndGet(); + if (mStethoHook.isEnabled()) { + mStethoHook.requestWillBeSent(new VolleyNetworkInspectorRequest(requestId, request)); + if (request.getBody() != null) { + mStethoHook.dataSent(String.valueOf(requestId), request.getBody().length, request.getBody().length); + } + } + + httpResponse = mHttpStack.performRequest(request, headers); + + StatusLine statusLine = httpResponse.getStatusLine(); + int statusCode = statusLine.getStatusCode(); + + responseHeaders = convertHeaders(httpResponse.getAllHeaders()); + + if (mStethoHook.isEnabled()) { + mStethoHook.responseHeadersReceived(new VolleyNetworkInspectorResponse(request, httpResponse, String.valueOf(request))); + } + + // Handle cache validation. + if (statusCode == HttpStatus.SC_NOT_MODIFIED) { + return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED, + request.getCacheEntry().data, responseHeaders, true); + } + + responseContents = entityToBytes(httpResponse.getEntity(), requestId); + // if the request is slow, log it. + long requestLifetime = SystemClock.elapsedRealtime() - requestStart; + logSlowRequests(requestLifetime, request, responseContents, statusLine); + + if (!statusCodeIsGood(statusCode)) { + 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); + } + } + } + } + + /** + * AHD added 6/28/2013 - we accept more than just 200 and 204 that the original BasicNetwork + * accepted. + */ + protected boolean statusCodeIsGood(int statusCode) { + return statusCode < 400; + } + + /** + * 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 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, int requestId) throws IOException, ServerError { + PoolingByteArrayOutputStream bytes = + new PoolingByteArrayOutputStream(mPool, (int) entity.getContentLength()); + byte[] buffer = null; + try { + + InputStream in = entity.getContent(); + + if (mStethoHook.isEnabled()) { + String contentEncoding = null; + + if (entity.getContentEncoding() != null) { + contentEncoding = entity.getContentEncoding().getValue(); + } else { + contentEncoding = "utf-8"; + } + + in = mStethoHook.interpretResponseStream(String.valueOf(requestId), + entity.getContentType().getValue(), + contentEncoding, + in, + new DefaultResponseHandler(mStethoHook, String.valueOf(requestId)) + ); + } + + 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. + */ + private static Map convertHeaders(Header[] headers) { + Map result = new HashMap(); + for (int i = 0; i < headers.length; i++) { + result.put(headers[i].getName(), headers[i].getValue()); + } + return result; + } +} diff --git a/stetho-volley/src/main/java/com/facebook/stetho/volley/VolleyNetworkInspectorRequest.java b/stetho-volley/src/main/java/com/facebook/stetho/volley/VolleyNetworkInspectorRequest.java new file mode 100644 index 00000000..1a26f832 --- /dev/null +++ b/stetho-volley/src/main/java/com/facebook/stetho/volley/VolleyNetworkInspectorRequest.java @@ -0,0 +1,113 @@ +package com.facebook.stetho.volley; + +import com.android.volley.AuthFailureError; +import com.android.volley.NetworkResponse; +import com.android.volley.Request; +import com.android.volley.toolbox.GsonRequest; +import com.facebook.stetho.inspector.network.NetworkEventReporter; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Map; +import java.util.SortedMap; +import java.util.TreeMap; + +import javax.annotation.Nullable; + +/** + * Created by briangriffey on 2/19/15. + */ +public class VolleyNetworkInspectorRequest implements NetworkEventReporter.InspectorRequest { + + private final String requestId; + private final Request request; + private ArrayList> orderedHeaders; + + public VolleyNetworkInspectorRequest(int requestId, Request request) throws AuthFailureError { + this.requestId = String.valueOf(requestId); + this.request = request; + orderedHeaders = new ArrayList<>(); + + if (request != null && request.getHeaders() != null) { + orderedHeaders.addAll(request.getHeaders().entrySet()); + } + } + + @Override + public String id() { + return requestId; + } + + @Override + public String friendlyName() { + return url(); + } + + @Nullable + @Override + public Integer friendlyNameExtra() { + return null; + } + + @Override + public String url() { + return request.getUrl(); + } + + @Override + public String method() { + return getMethodName(request.getMethod()); + } + + @Nullable + @Override + public byte[] body() throws IOException { + try { + return request.getBody(); + } catch (AuthFailureError e) { + return null; + } + } + + @Override + public int headerCount() { + return orderedHeaders.size(); + } + + @Override + public String headerName(int i) { + return orderedHeaders.get(i).getKey(); + } + + @Override + public String headerValue(int i) { + return orderedHeaders.get(i).getValue(); + } + + @Nullable + @Override + public String firstHeaderValue(String s) { + for (Map.Entry entry : orderedHeaders) { + if (entry.getKey().equals(s)) { + return entry.getValue(); + } + } + + return null; + } + + public String getMethodName(int methodCode) { + switch (methodCode) { + case Request.Method.GET: + return "GET"; + case Request.Method.POST: + return "POST"; + case Request.Method.DELETE: + return "DELETE"; + case Request.Method.PUT: + return "PUT"; + default: + return "GET"; + } + } +} diff --git a/stetho-volley/src/main/java/com/facebook/stetho/volley/VolleyNetworkInspectorResponse.java b/stetho-volley/src/main/java/com/facebook/stetho/volley/VolleyNetworkInspectorResponse.java new file mode 100644 index 00000000..65d75dcd --- /dev/null +++ b/stetho-volley/src/main/java/com/facebook/stetho/volley/VolleyNetworkInspectorResponse.java @@ -0,0 +1,90 @@ +package com.facebook.stetho.volley; + +import com.android.volley.Request; +import com.facebook.stetho.inspector.network.NetworkEventReporter; + +import org.apache.http.HttpResponse; + +import javax.annotation.Nullable; + +/** + * Response tracker for Stetho, Encapsulates all of the info from an {@link org.apache.http.HttpResponse} + * and {@link com.android.volley.Request} + * Created by briangriffey on 2/19/15. + */ +public class VolleyNetworkInspectorResponse implements NetworkEventReporter.InspectorResponse { + + + private final HttpResponse response; + private final String requestId; + private final Request request; + + /** + * Constructor that takes the minimum amount of into required to fill out a response + * + * @param request The request that spawned this response + * @param response The actual response object + * @param requestId a requestid that tracks through the call + */ + public VolleyNetworkInspectorResponse(Request request, HttpResponse response, String requestId) { + this.response = response; + this.requestId = requestId; + this.request = request; + } + + @Override + public int headerCount() { + return response.getAllHeaders().length; + } + + @Override + public String headerName(int i) { + return response.getAllHeaders()[i].getName(); + } + + @Override + public String headerValue(int i) { + return response.getAllHeaders()[i].getValue(); + } + + @Nullable + @Override + public String firstHeaderValue(String s) { + return null; + } + + @Override + public String requestId() { + return requestId; + } + + @Override + public String url() { + return request.getUrl(); + } + + @Override + public int statusCode() { + return response.getStatusLine().getStatusCode(); + } + + @Override + public String reasonPhrase() { + return null; + } + + @Override + public boolean connectionReused() { + return false; + } + + @Override + public int connectionId() { + return 0; + } + + @Override + public boolean fromDiskCache() { + return false; + } +}