Skip to content

Commit

Permalink
Add StethoInterceptor for OkHttp 2.2.0+
Browse files Browse the repository at this point in the history
This makes it possible to enable Stetho network inspection by a one-line
change to existing projects that use OkHttp 2.2.0+.  This is made
possible by the new Interceptors feature:

  https://github.com/square/okhttp/wiki/Interceptors
  • Loading branch information
Josh Guilfoyle committed Feb 3, 2015
1 parent 5ac8e9c commit eef68e1
Show file tree
Hide file tree
Showing 7 changed files with 498 additions and 1 deletion.
1 change: 1 addition & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
include ':stetho'
include ':stetho-urlconnection'
include ':stetho-okhttp'
include ':stetho-sample'
1 change: 1 addition & 0 deletions stetho-okhttp/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
51 changes: 51 additions & 0 deletions stetho-okhttp/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
apply plugin: 'com.android.library'
apply plugin: 'robolectric'

android {
compileSdkVersion 21
buildToolsVersion "21.1.2"

defaultConfig {
minSdkVersion 9
targetSdkVersion 21
versionCode 1
versionName "1.0"
}

sourceSets {
androidTest {
setRoot('src/test')
}
}

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'
}
}

robolectric {
include '**/*Test.class'
exclude '**/espresso/**/*.class'
}

dependencies {
compile project(':stetho')
compile 'com.google.code.findbugs:jsr305:2.0.1'
//noinspection GradleDynamicVersion
compile 'com.squareup.okhttp:okhttp:2.2.0+'

androidTestCompile 'junit:junit:4.12'
androidTestCompile('org.robolectric:robolectric:2.4') {
exclude module: 'commons-logging'
exclude module: 'httpclient'
}
androidTestCompile 'org.powermock:powermock-api-mockito:1.6.1'
androidTestCompile 'org.powermock:powermock-module-junit4:1.6.1'

// Needed for Robolectric and PowerMock to be combined in a single test.
androidTestCompile 'org.powermock:powermock-module-junit4-rule:1.6.1'
androidTestCompile 'org.powermock:powermock-classloading-xstream:1.6.1'
}
7 changes: 7 additions & 0 deletions stetho-okhttp/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.facebook.stetho.okhttp">

<application />

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
package com.facebook.stetho.okhttp;

import javax.annotation.Nullable;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.atomic.AtomicInteger;

import com.facebook.stetho.inspector.network.DefaultResponseHandler;
import com.facebook.stetho.inspector.network.NetworkEventReporter;
import com.facebook.stetho.inspector.network.NetworkEventReporterImpl;

import com.squareup.okhttp.Connection;
import com.squareup.okhttp.Interceptor;
import com.squareup.okhttp.MediaType;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.RequestBody;
import com.squareup.okhttp.Response;
import com.squareup.okhttp.ResponseBody;
import okio.BufferedSink;
import okio.BufferedSource;
import okio.Okio;

/**
* Provides easy integration with <a href="http://square.github.io/okhttp/">OkHttp</a> 2.2.0+
* by way of the new <a href="https://github.com/square/okhttp/wiki/Interceptors">Interceptor</a>
* system. To use:
* <pre>
* OkHttpClient client = new OkHttpClient();
* client.networkInterceptors().add(new StethoInterceptor());
* </pre>
*/
public class StethoInterceptor implements Interceptor {
private final NetworkEventReporter mEventReporter = NetworkEventReporterImpl.get();

private final AtomicInteger mNextRequestId = new AtomicInteger(0);

@Override
public Response intercept(Chain chain) throws IOException {
String requestId = String.valueOf(mNextRequestId.getAndIncrement());

Request request = chain.request();

int requestSize = 0;
if (mEventReporter.isEnabled()) {
OkHttpInspectorRequest inspectorRequest = new OkHttpInspectorRequest(requestId, request);
mEventReporter.requestWillBeSent(inspectorRequest);
byte[] requestBody = inspectorRequest.body();
if (requestBody != null) {
requestSize += requestBody.length;
}
}

Response response;
try {
response = chain.proceed(request);
} catch (IOException e) {
if (mEventReporter.isEnabled()) {
mEventReporter.httpExchangeFailed(requestId, e.toString());
}
throw e;
}

if (mEventReporter.isEnabled()) {
if (requestSize > 0) {
mEventReporter.dataSent(requestId, requestSize, requestSize);
}

Connection connection = chain.connection();
mEventReporter.responseHeadersReceived(
new OkHttpInspectorResponse(
requestId,
request,
response,
connection));

ResponseBody body = response.body();
MediaType contentType = null;
InputStream responseStream = null;
if (body != null) {
contentType = body.contentType();
responseStream = body.byteStream();
}

responseStream = mEventReporter.interpretResponseStream(
requestId,
contentType != null ? contentType.toString() : null,
responseStream,
new DefaultResponseHandler(mEventReporter, requestId));
if (responseStream != null) {
response = response.newBuilder()
.body(new ForwardingResponseBody(body, responseStream))
.build();
}
}

return response;
}

private static class OkHttpInspectorRequest implements NetworkEventReporter.InspectorRequest {
private final String mRequestId;
private final Request mRequest;
private byte[] mBody;
private boolean mBodyGenerated;

public OkHttpInspectorRequest(String requestId, Request request) {
mRequestId = requestId;
mRequest = request;
}

@Override
public String id() {
return mRequestId;
}

@Override
public String friendlyName() {
// Hmm, can we do better? tag() perhaps?
return null;
}

@Nullable
@Override
public Integer friendlyNameExtra() {
return null;
}

@Override
public String url() {
return mRequest.urlString();
}

@Override
public String method() {
return mRequest.method();
}

@Nullable
@Override
public byte[] body() throws IOException {
if (!mBodyGenerated) {
mBodyGenerated = true;
mBody = generateBody();
}
return mBody;
}

private byte[] generateBody() throws IOException {
RequestBody body = mRequest.body();
if (body != null) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
BufferedSink sink = Okio.buffer(Okio.sink(out));
body.writeTo(sink);
sink.flush();
return out.toByteArray();
} else {
return null;
}
}

@Override
public int headerCount() {
return mRequest.headers().size();
}

@Override
public String headerName(int index) {
return mRequest.headers().name(index);
}

@Override
public String headerValue(int index) {
return mRequest.headers().value(index);
}

@Nullable
@Override
public String firstHeaderValue(String name) {
return mRequest.header(name);
}
}

private static class OkHttpInspectorResponse implements NetworkEventReporter.InspectorResponse {
private final String mRequestId;
private final Request mRequest;
private final Response mResponse;
private final Connection mConnection;

public OkHttpInspectorResponse(
String requestId,
Request request,
Response response,
Connection connection) {
mRequestId = requestId;
mRequest = request;
mResponse = response;
mConnection = connection;
}

@Override
public String requestId() {
return mRequestId;
}

@Override
public String url() {
return mRequest.urlString();
}

@Override
public int statusCode() {
return mResponse.code();
}

@Override
public String reasonPhrase() {
return mResponse.message();
}

@Override
public boolean connectionReused() {
// Not sure...
return false;
}

@Override
public int connectionId() {
return mConnection.hashCode();
}

@Override
public boolean fromDiskCache() {
return mResponse.cacheResponse() != null;
}

@Override
public int headerCount() {
return mResponse.headers().size();
}

@Override
public String headerName(int index) {
return mResponse.headers().name(index);
}

@Override
public String headerValue(int index) {
return mResponse.headers().value(index);
}

@Nullable
@Override
public String firstHeaderValue(String name) {
return mResponse.header(name);
}
}

private static class ForwardingResponseBody extends ResponseBody {
private final ResponseBody mBody;
private final BufferedSource mInterceptedSource;

public ForwardingResponseBody(ResponseBody body, InputStream interceptedStream) {
mBody = body;
mInterceptedSource = Okio.buffer(Okio.source(interceptedStream));
}

@Override
public MediaType contentType() {
return mBody.contentType();
}

@Override
public long contentLength() {
return mBody.contentLength();
}

@Override
public BufferedSource source() {
// close on the delegating body will actually close this intercepted source, but it
// was derived from mBody.byteStream() therefore the close will be forwarded all the
// way to the original.
return mInterceptedSource;
}
}
}
Loading

0 comments on commit eef68e1

Please sign in to comment.