/* * Copyright (c) 2014-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. */ package com.facebook.stetho.sample; import com.facebook.stetho.urlconnection.ByteArrayRequestEntity; import com.facebook.stetho.urlconnection.SimpleRequestEntity; import com.facebook.stetho.urlconnection.StethoURLConnectionManager; import javax.annotation.Nullable; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.zip.GZIPInputStream; /** * Very simple centralized network middleware for illustration purposes. */ public class Networker { private static Networker sInstance; private final Executor sExecutor = Executors.newFixedThreadPool(4); private static final int READ_TIMEOUT_MS = 10000; private static final int CONNECT_TIMEOUT_MS = 15000; private static final String HEADER_ACCEPT_ENCODING = "Accept-Encoding"; private static final String GZIP_ENCODING = "gzip"; public static synchronized Networker get() { if (sInstance == null) { sInstance = new Networker(); } return sInstance; } private Networker() { } public void submit(HttpRequest request, Callback callback) { sExecutor.execute(new HttpRequestTask(request, callback)); } private class HttpRequestTask implements Runnable { private final HttpRequest request; private final Callback callback; private final StethoURLConnectionManager stethoManager; public HttpRequestTask(HttpRequest request, Callback callback) { this.request = request; this.callback = callback; stethoManager = new StethoURLConnectionManager(request.friendlyName); } @Override public void run() { try { HttpResponse response = doFetch(); callback.onResponse(response); } catch (IOException e) { callback.onFailure(e); } } private HttpResponse doFetch() throws IOException { HttpURLConnection conn = configureAndConnectRequest(); try { ByteArrayOutputStream out = new ByteArrayOutputStream(); InputStream rawStream = conn.getInputStream(); try { // Let Stetho see the raw, possibly compressed stream. rawStream = stethoManager.interpretResponseStream(rawStream); InputStream decompressedStream = applyDecompressionIfApplicable(conn, rawStream); if (decompressedStream != null) { copy(decompressedStream, out, new byte[1024]); } } finally { if (rawStream != null) { rawStream.close(); } } return new HttpResponse(conn.getResponseCode(), out.toByteArray()); } finally { conn.disconnect(); } } private HttpURLConnection configureAndConnectRequest() throws IOException { URL url = new URL(request.url); // Note that this does not actually create a new connection so it is appropriate to // defer preConnect until after the HttpURLConnection instance is configured. Do not // invoke connect, conn.getInputStream, conn.getOutputStream, etc before calling // preConnect! HttpURLConnection conn = (HttpURLConnection)url.openConnection(); try { conn.setReadTimeout(READ_TIMEOUT_MS); conn.setConnectTimeout(CONNECT_TIMEOUT_MS); conn.setRequestMethod(request.method.toString()); // Adding this disables transparent gzip compression so that we can intercept // the raw stream and display the correct response body size. requestDecompression(conn); SimpleRequestEntity requestEntity = null; if (request.body != null) { requestEntity = new ByteArrayRequestEntity(request.body); } stethoManager.preConnect(conn, requestEntity); try { if (request.method == HttpMethod.POST) { if (requestEntity == null) { throw new IllegalStateException("POST requires an entity"); } conn.setDoOutput(true); requestEntity.writeTo(conn.getOutputStream()); } // Ensure that we are connected after this point. Note that getOutputStream above will // also connect and exchange HTTP messages. conn.connect(); stethoManager.postConnect(); return conn; } catch (IOException inner) { // This must only be called after preConnect. Failures before that cannot be // represented since the request has not yet begun according to Stetho. stethoManager.httpExchangeFailed(inner); throw inner; } } catch (IOException outer) { conn.disconnect(); throw outer; } } } private static void requestDecompression(HttpURLConnection conn) { conn.setRequestProperty(HEADER_ACCEPT_ENCODING, GZIP_ENCODING); } @Nullable private static InputStream applyDecompressionIfApplicable( HttpURLConnection conn, @Nullable InputStream in) throws IOException { if (in != null && GZIP_ENCODING.equals(conn.getContentEncoding())) { return new GZIPInputStream(in); } return in; } private static void copy(InputStream in, OutputStream out, byte[] buf) throws IOException { if (in == null) { return; } int n; while ((n = in.read(buf)) != -1) { out.write(buf, 0, n); } } public static class HttpRequest { public final String friendlyName; public final HttpMethod method; public final String url; public final byte[] body; public static Builder newBuilder() { return new Builder(); } HttpRequest(Builder b) { if (b.method == HttpMethod.POST) { if (b.body == null) { throw new IllegalArgumentException("POST must have a body"); } } else if (b.method == HttpMethod.GET) { if (b.body != null) { throw new IllegalArgumentException("GET cannot have a body"); } } this.friendlyName = b.friendlyName; this.method = b.method; this.url = b.url; this.body = b.body; } public static class Builder { private String friendlyName; private Networker.HttpMethod method; private String url; private byte[] body = null; Builder() { } public Builder friendlyName(String friendlyName) { this.friendlyName = friendlyName; return this; } public Builder method(Networker.HttpMethod method) { this.method = method; return this; } public Builder url(String url) { this.url = url; return this; } public Builder body(byte[] body) { this.body = body; return this; } public HttpRequest build() { return new HttpRequest(this); } } } public static enum HttpMethod { GET, POST } public static class HttpResponse { public final int statusCode; public final byte[] body; HttpResponse(int statusCode, byte[] body) { this.statusCode = statusCode; this.body = body; } } public interface Callback { public void onResponse(HttpResponse result); public void onFailure(IOException e); } }