/* * 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.inspector; import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.net.Uri; import com.facebook.stetho.common.ProcessUtil; import com.facebook.stetho.server.http.ExactPathMatcher; import com.facebook.stetho.server.http.HandlerRegistry; import com.facebook.stetho.server.http.HttpHandler; import com.facebook.stetho.server.http.HttpStatus; import com.facebook.stetho.server.SocketLike; import com.facebook.stetho.server.http.LightHttpBody; import com.facebook.stetho.server.http.LightHttpRequest; import com.facebook.stetho.server.http.LightHttpResponse; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import javax.annotation.Nullable; /** * Provides sufficient responses to convince Chrome's {@code chrome://inspect/devices} that we're * "one of them". Note that we are being discovered automatically by the name of our socket * as defined in {@link LocalSocketHttpServer}. After discovery, we're required to provide * some context on how exactly to display and inspect what we have. */ public class ChromeDiscoveryHandler implements HttpHandler { private static final String PAGE_ID = "1"; private static final String PATH_PAGE_LIST = "/json"; private static final String PATH_VERSION = "/json/version"; private static final String PATH_ACTIVATE = "/json/activate/" + PAGE_ID; /** * Latest version of the WebKit Inspector UI that we've tested again (ideally). */ private static final String WEBKIT_REV = "@188492"; private static final String WEBKIT_VERSION = "537.36 (" + WEBKIT_REV + ")"; private static final String USER_AGENT = "Stetho"; /** * Structured version of the WebKit Inspector protocol that we understand. */ private static final String PROTOCOL_VERSION = "1.1"; private final Context mContext; private final String mInspectorPath; @Nullable private LightHttpBody mVersionResponse; @Nullable private LightHttpBody mPageListResponse; public ChromeDiscoveryHandler(Context context, String inspectorPath) { mContext = context; mInspectorPath = inspectorPath; } public void register(HandlerRegistry registry) { registry.register(new ExactPathMatcher(PATH_PAGE_LIST), this); registry.register(new ExactPathMatcher(PATH_VERSION), this); registry.register(new ExactPathMatcher(PATH_ACTIVATE), this); } @Override public boolean handleRequest(SocketLike socket, LightHttpRequest request, LightHttpResponse response) { String path = request.uri.getPath(); try { if (PATH_VERSION.equals(path)) { handleVersion(response); } else if (PATH_PAGE_LIST.equals(path)) { handlePageList(response); } else if (PATH_ACTIVATE.equals(path)) { handleActivate(response); } else { response.code = HttpStatus.HTTP_NOT_IMPLEMENTED; response.reasonPhrase = "Not implemented"; response.body = LightHttpBody.create("No support for " + path + "\n", "text/plain"); } } catch (JSONException e) { response.code = HttpStatus.HTTP_INTERNAL_SERVER_ERROR; response.reasonPhrase = "Internal server error"; response.body = LightHttpBody.create(e.toString() + "\n", "text/plain"); } return true; } private void handleVersion(LightHttpResponse response) throws JSONException { if (mVersionResponse == null) { JSONObject reply = new JSONObject(); reply.put("WebKit-Version", WEBKIT_VERSION); reply.put("User-Agent", USER_AGENT); reply.put("Protocol-Version", PROTOCOL_VERSION); reply.put("Browser", getAppLabelAndVersion()); reply.put("Android-Package", mContext.getPackageName()); mVersionResponse = LightHttpBody.create(reply.toString(), "application/json"); } setSuccessfulResponse(response, mVersionResponse); } private void handlePageList(LightHttpResponse response) throws JSONException { if (mPageListResponse == null) { JSONArray reply = new JSONArray(); JSONObject page = new JSONObject(); page.put("type", "app"); page.put("title", makeTitle()); page.put("id", PAGE_ID); page.put("description", ""); page.put("webSocketDebuggerUrl", "ws://" + mInspectorPath); Uri chromeFrontendUrl = new Uri.Builder() .scheme("http") .authority("chrome-devtools-frontend.appspot.com") .appendEncodedPath("serve_rev") .appendEncodedPath(WEBKIT_REV) .appendEncodedPath("devtools.html") .appendQueryParameter("ws", mInspectorPath) .build(); page.put("devtoolsFrontendUrl", chromeFrontendUrl.toString()); reply.put(page); mPageListResponse = LightHttpBody.create(reply.toString(), "application/json"); } setSuccessfulResponse(response, mPageListResponse); } private String makeTitle() { StringBuilder b = new StringBuilder(); b.append(getAppLabel()); b.append(" (powered by Stetho)"); String processName = ProcessUtil.getProcessName(); int colonIndex = processName.indexOf(':'); if (colonIndex >= 0) { String nonDefaultProcessName = processName.substring(colonIndex); b.append(nonDefaultProcessName); } return b.toString(); } private void handleActivate(LightHttpResponse response) { // Arbitrary response seem acceptable :) setSuccessfulResponse( response, LightHttpBody.create("Target activation ignored\n", "text/plain")); } private static void setSuccessfulResponse( LightHttpResponse response, LightHttpBody body) { response.code = HttpStatus.HTTP_OK; response.reasonPhrase = "OK"; response.body = body; } private String getAppLabelAndVersion() { StringBuilder b = new StringBuilder(); PackageManager pm = mContext.getPackageManager(); b.append(getAppLabel()); b.append('/'); try { PackageInfo info = pm.getPackageInfo(mContext.getPackageName(), 0 /* flags */); b.append(info.versionName); } catch (PackageManager.NameNotFoundException e) { throw new RuntimeException(e); } return b.toString(); } private CharSequence getAppLabel() { PackageManager pm = mContext.getPackageManager(); return pm.getApplicationLabel(mContext.getApplicationInfo()); } }