/* * 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; import android.app.Application; import android.content.Context; import android.content.SharedPreferences; import android.os.Build; import com.facebook.stetho.common.LogUtil; import com.facebook.stetho.common.Util; import com.facebook.stetho.dumpapp.DumpappHttpSocketLikeHandler; import com.facebook.stetho.dumpapp.DumpappSocketLikeHandler; import com.facebook.stetho.dumpapp.Dumper; import com.facebook.stetho.dumpapp.DumperPlugin; import com.facebook.stetho.dumpapp.plugins.CrashDumperPlugin; import com.facebook.stetho.dumpapp.plugins.FilesDumperPlugin; import com.facebook.stetho.dumpapp.plugins.HprofDumperPlugin; import com.facebook.stetho.dumpapp.plugins.SharedPreferencesDumperPlugin; import com.facebook.stetho.inspector.DevtoolsSocketHandler; import com.facebook.stetho.inspector.console.RuntimeReplFactory; import com.facebook.stetho.inspector.database.ContentProviderDatabaseDriver; import com.facebook.stetho.inspector.database.DatabaseDriver2Adapter; import com.facebook.stetho.inspector.database.DatabaseFilesProvider; import com.facebook.stetho.inspector.database.DefaultDatabaseConnectionProvider; import com.facebook.stetho.inspector.database.DefaultDatabaseFilesProvider; import com.facebook.stetho.inspector.database.SqliteDatabaseDriver; import com.facebook.stetho.inspector.elements.DescriptorProvider; import com.facebook.stetho.inspector.elements.Document; import com.facebook.stetho.inspector.elements.DocumentProviderFactory; import com.facebook.stetho.inspector.elements.android.ActivityTracker; import com.facebook.stetho.inspector.elements.android.AndroidDocumentConstants; import com.facebook.stetho.inspector.elements.android.AndroidDocumentProviderFactory; import com.facebook.stetho.inspector.protocol.ChromeDevtoolsDomain; import com.facebook.stetho.inspector.protocol.module.CSS; import com.facebook.stetho.inspector.protocol.module.Console; import com.facebook.stetho.inspector.protocol.module.DOM; import com.facebook.stetho.inspector.protocol.module.DOMStorage; import com.facebook.stetho.inspector.protocol.module.Database; import com.facebook.stetho.inspector.protocol.module.DatabaseConstants; import com.facebook.stetho.inspector.protocol.module.DatabaseDriver2; import com.facebook.stetho.inspector.protocol.module.Debugger; import com.facebook.stetho.inspector.protocol.module.HeapProfiler; import com.facebook.stetho.inspector.protocol.module.Inspector; import com.facebook.stetho.inspector.protocol.module.Network; import com.facebook.stetho.inspector.protocol.module.Page; import com.facebook.stetho.inspector.protocol.module.Profiler; import com.facebook.stetho.inspector.protocol.module.Runtime; import com.facebook.stetho.inspector.protocol.module.Worker; import com.facebook.stetho.inspector.runtime.RhinoDetectingRuntimeReplFactory; import com.facebook.stetho.server.AddressNameHelper; import com.facebook.stetho.server.LazySocketHandler; import com.facebook.stetho.server.LocalSocketServer; import com.facebook.stetho.server.ServerManager; import com.facebook.stetho.server.ProtocolDetectingSocketHandler; import com.facebook.stetho.server.SocketHandler; import com.facebook.stetho.server.SocketHandlerFactory; import javax.annotation.Nullable; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; /** * Initialization and configuration entry point for the Stetho debugging system. Simple usage with * default plugins and features enabled: * <p /> * <pre> * Stetho.initializeWithDefaults(context) * </pre> * <p /> * For more advanced configuration, see {@link #newInitializerBuilder(Context)} or * the {@code stetho-sample} for more information. */ public class Stetho { private Stetho() { } /** * Construct a simple initializer helper which allows you to customize stetho behaviour * with additional features, plugins, etc. See {@link DefaultDumperPluginsBuilder} and * {@link DefaultInspectorModulesBuilder} for more information. * <p /> * For simple use cases, consider {@link #initializeWithDefaults(Context)}. */ public static InitializerBuilder newInitializerBuilder(Context context) { return new InitializerBuilder(context); } /** * Start the listening server. Most of the heavy lifting initialization is deferred until the * first socket connection is received, allowing this to be safely used for debug builds on * even low-end hardware without noticeably affecting performance. */ public static void initializeWithDefaults(final Context context) { initialize(new Initializer(context) { @Override protected Iterable<DumperPlugin> getDumperPlugins() { return new DefaultDumperPluginsBuilder(context).finish(); } @Override protected Iterable<ChromeDevtoolsDomain> getInspectorModules() { return new DefaultInspectorModulesBuilder(context).finish(); } }); } /** * Start the listening service, providing a custom initializer as per * {@link #newInitializerBuilder}. * * @see #initializeWithDefaults(Context) */ public static void initialize(final Initializer initializer) { // Hook activity tracking so that after Stetho is attached we can figure out what // activities are present. boolean isTrackingActivities = ActivityTracker.get().beginTrackingIfPossible( (Application)initializer.mContext.getApplicationContext()); if (!isTrackingActivities) { LogUtil.w("Automatic activity tracking not available on this API level, caller must invoke " + "ActivityTracker methods manually!"); } initializer.start(); } public static DumperPluginsProvider defaultDumperPluginsProvider(final Context context) { return new DumperPluginsProvider() { @Override public Iterable<DumperPlugin> get() { return new DefaultDumperPluginsBuilder(context).finish(); } }; } public static InspectorModulesProvider defaultInspectorModulesProvider(final Context context) { return new InspectorModulesProvider() { @Override public Iterable<ChromeDevtoolsDomain> get() { return new DefaultInspectorModulesBuilder(context).finish(); } }; } private static class PluginBuilder<T> { private final Set<String> mProvidedNames = new HashSet<>(); private final Set<String> mRemovedNames = new HashSet<>(); private final ArrayList<T> mPlugins = new ArrayList<>(); private boolean mFinished; public void provide(String name, T plugin) { throwIfFinished(); mPlugins.add(plugin); mProvidedNames.add(name); } public void provideIfDesired(String name, T plugin) { throwIfFinished(); if (!mRemovedNames.contains(name)) { if (mProvidedNames.add(name)) { mPlugins.add(plugin); } } } public void remove(String pluginName) { throwIfFinished(); mRemovedNames.remove(pluginName); } private void throwIfFinished() { if (mFinished) { throw new IllegalStateException("Must not continue to build after finish()"); } } public Iterable<T> finish() { mFinished = true; return mPlugins; } } /** * Convenience mechanism to extend the default set of dumper plugins provided by Stetho. * * @see #initializeWithDefaults(Context) */ public static final class DefaultDumperPluginsBuilder { private final Context mContext; private final PluginBuilder<DumperPlugin> mDelegate = new PluginBuilder<>(); public DefaultDumperPluginsBuilder(Context context) { mContext = context; } public DefaultDumperPluginsBuilder provide(DumperPlugin plugin) { mDelegate.provide(plugin.getName(), plugin); return this; } private DefaultDumperPluginsBuilder provideIfDesired(DumperPlugin plugin) { mDelegate.provideIfDesired(plugin.getName(), plugin); return this; } public DefaultDumperPluginsBuilder remove(String pluginName) { mDelegate.remove(pluginName); return this; } public Iterable<DumperPlugin> finish() { provideIfDesired(new HprofDumperPlugin(mContext)); provideIfDesired(new SharedPreferencesDumperPlugin(mContext)); provideIfDesired(new CrashDumperPlugin()); provideIfDesired(new FilesDumperPlugin(mContext)); return mDelegate.finish(); } } /** * Configuration mechanism to customize the behaviour of the standard set of inspector * modules satisfying the Chrome DevTools protocol. Note that while it is still technically * possible to manually control these modules, this API is strongly discouraged and will not * necessarily be supported in future releases. */ public static final class DefaultInspectorModulesBuilder { private final Application mContext; private final PluginBuilder<ChromeDevtoolsDomain> mDelegate = new PluginBuilder<>(); @Nullable private DocumentProviderFactory mDocumentProvider; @Nullable private RuntimeReplFactory mRuntimeRepl; @Nullable private DatabaseFilesProvider mDatabaseFilesProvider; @Nullable private List<DatabaseDriver2> mDatabaseDrivers; private boolean mExcludeSqliteDatabaseDriver; public DefaultInspectorModulesBuilder(Context context) { mContext = (Application)context.getApplicationContext(); } /** * Provide a custom document provider factory which can operate on the logical DOM exposed to * Chrome in the Elements tab. An Android View hierarchy instance is provided by * default if this method is not called. * <p /> * <i>Experimental.</i> This API may be changed or removed in the future. */ public DefaultInspectorModulesBuilder documentProvider(DocumentProviderFactory factory) { mDocumentProvider = factory; return this; } /** * Provide a custom runtime REPL (read-eval-print loop) implementation for the Console tab. * By default an implementation will be provided for you that automatically detects * the existence of {@code stetho-js-rhino} (Mozilla's Rhino engine) and uses it if available. * <p /> * To customize the Rhino implementation, see {@code stetho-js-rhino} documentation. */ public DefaultInspectorModulesBuilder runtimeRepl(RuntimeReplFactory factory) { mRuntimeRepl = factory; return this; } /** * Customize the location of database files that Stetho will propogate in the UI. Android's * {@link Context#getDatabasePath} method will be used by default if not overridden here. * * <p>This method is deprecated and instead it is recommended that you explicitly * configure the {@link SqliteDatabaseDriver} as with:</p> * <pre> * provideDatabaseDriver( * new SqliteDatabaseDriver( * context, * new MyDatabaseFilesProvider(...), * new DefaultDatabaseConnectionProvider(...))) * </pre> * * @deprecated Use {@link #provideDatabaseDriver(DatabaseDriver2)} with * {@link SqliteDatabaseDriver} explicitly. */ @Deprecated public DefaultInspectorModulesBuilder databaseFiles(DatabaseFilesProvider provider) { mDatabaseFilesProvider = provider; return this; } /** * @deprecated Convert your custom database driver to {@link DatabaseDriver2}. */ @Deprecated public DefaultInspectorModulesBuilder provideDatabaseDriver(Database.DatabaseDriver databaseDriver) { provideDatabaseDriver(new DatabaseDriver2Adapter(databaseDriver)); return this; } /** * Extend and provide additional database drivers. Stetho provides two database * drivers by default, with the option for developers to provide their own: * <ol> * <li>{@link SqliteDatabaseDriver} - Presents SQLite databases.</li> * <li>{@link ContentProviderDatabaseDriver} - Configure and present content provider * data.</li> * </ol> * * <p>Stetho assumes the {@link SqliteDatabaseDriver} should be installed if * no driver of that type is provided and {@link #excludeSqliteDatabaseDriver} is not * used.</p> */ public DefaultInspectorModulesBuilder provideDatabaseDriver(DatabaseDriver2 databaseDriver) { if (mDatabaseDrivers == null) { mDatabaseDrivers = new ArrayList<>(); } mDatabaseDrivers.add(databaseDriver); return this; } /** * Do not automatically provide the {@link SqliteDatabaseDriver} instance. The instance * is provided by default for backwards compatibility purposes and simplicity of API, with * this API provided to disable that functionality if desired. */ public DefaultInspectorModulesBuilder excludeSqliteDatabaseDriver(boolean exclude) { mExcludeSqliteDatabaseDriver = exclude; return this; } /** * Provide either a new domain module or override an existing one. * * @deprecated This fine-grained control of the devtools modules is no longer supportable * given the lack of isolation of modules in the actual protocol (many cross dependencies * emerge when you implement more and more of the real protocol). */ @Deprecated public DefaultInspectorModulesBuilder provide(ChromeDevtoolsDomain module) { mDelegate.provide(module.getClass().getName(), module); return this; } private DefaultInspectorModulesBuilder provideIfDesired(ChromeDevtoolsDomain module) { mDelegate.provideIfDesired(module.getClass().getName(), module); return this; } /** * Remove an existing domain module. * * @deprecated This fine-grained control of the devtools modules is no longer supportable * given the lack of isolation of modules in the actual protocol (many cross dependencies * emerge when you implement more and more of the real protocol). */ @Deprecated public DefaultInspectorModulesBuilder remove(String moduleName) { mDelegate.remove(moduleName); return this; } public Iterable<ChromeDevtoolsDomain> finish() { provideIfDesired(new Console()); provideIfDesired(new Debugger()); DocumentProviderFactory documentModel = resolveDocumentProvider(); if (documentModel != null) { Document document = new Document(documentModel); provideIfDesired(new DOM(document)); provideIfDesired(new CSS(document)); } provideIfDesired(new DOMStorage(mContext)); provideIfDesired(new HeapProfiler()); provideIfDesired(new Inspector()); provideIfDesired(new Network(mContext)); provideIfDesired(new Page(mContext)); provideIfDesired(new Profiler()); provideIfDesired( new Runtime( mRuntimeRepl != null ? mRuntimeRepl : new RhinoDetectingRuntimeReplFactory(mContext))); provideIfDesired(new Worker()); if (Build.VERSION.SDK_INT >= DatabaseConstants.MIN_API_LEVEL) { Database database = new Database(); boolean hasSqliteDatabaseDriver = false; if (mDatabaseDrivers != null) { for (DatabaseDriver2 databaseDriver : mDatabaseDrivers) { database.add(databaseDriver); if (databaseDriver instanceof SqliteDatabaseDriver) { hasSqliteDatabaseDriver = true; } } } if (!hasSqliteDatabaseDriver && !mExcludeSqliteDatabaseDriver) { database.add( new SqliteDatabaseDriver(mContext, mDatabaseFilesProvider != null ? mDatabaseFilesProvider : new DefaultDatabaseFilesProvider(mContext), new DefaultDatabaseConnectionProvider())); } provideIfDesired(database); } return mDelegate.finish(); } @Nullable private DocumentProviderFactory resolveDocumentProvider() { if (mDocumentProvider != null) { return mDocumentProvider; } if (Build.VERSION.SDK_INT >= AndroidDocumentConstants.MIN_API_LEVEL) { return new AndroidDocumentProviderFactory(mContext, Collections.<DescriptorProvider>emptyList()); } return null; } } /** * Callers can choose to subclass this directly to provide the initialization configuration * or they can construct a concrete instance using {@link #newInitializerBuilder(Context)}. */ public static abstract class Initializer { private final Context mContext; protected Initializer(Context context) { mContext = context.getApplicationContext(); } @Nullable protected abstract Iterable<DumperPlugin> getDumperPlugins(); @Nullable protected abstract Iterable<ChromeDevtoolsDomain> getInspectorModules(); final void start() { // Note that _devtools_remote is a magic suffix understood by Chrome which causes // the discovery process to begin. LocalSocketServer server = new LocalSocketServer( "main", AddressNameHelper.createCustomAddress("_devtools_remote"), new LazySocketHandler(new RealSocketHandlerFactory())); ServerManager serverManager = new ServerManager(server); serverManager.start(); } private class RealSocketHandlerFactory implements SocketHandlerFactory { @Override public SocketHandler create() { ProtocolDetectingSocketHandler socketHandler = new ProtocolDetectingSocketHandler(mContext); Iterable<DumperPlugin> dumperPlugins = getDumperPlugins(); if (dumperPlugins != null) { Dumper dumper = new Dumper(dumperPlugins); socketHandler.addHandler( new ProtocolDetectingSocketHandler.ExactMagicMatcher( DumpappSocketLikeHandler.PROTOCOL_MAGIC), new DumpappSocketLikeHandler(dumper)); // Support the old HTTP-based protocol since it's relatively straight forward to do. DumpappHttpSocketLikeHandler legacyHandler = new DumpappHttpSocketLikeHandler(dumper); socketHandler.addHandler( new ProtocolDetectingSocketHandler.ExactMagicMatcher( "GET /dumpapp".getBytes()), legacyHandler); socketHandler.addHandler( new ProtocolDetectingSocketHandler.ExactMagicMatcher( "POST /dumpapp".getBytes()), legacyHandler); } Iterable<ChromeDevtoolsDomain> inspectorModules = getInspectorModules(); if (inspectorModules != null) { socketHandler.addHandler( new ProtocolDetectingSocketHandler.AlwaysMatchMatcher(), new DevtoolsSocketHandler(mContext, inspectorModules)); } return socketHandler; } } } /** * Configure what services are to be enabled in this instance of Stetho. */ public static class InitializerBuilder { final Context mContext; @Nullable DumperPluginsProvider mDumperPlugins; @Nullable InspectorModulesProvider mInspectorModules; private InitializerBuilder(Context context) { mContext = context.getApplicationContext(); } /** * Enable use of the {@code dumpapp} system. This is an extension to Stetho which allows * developers to configure custom debug endpoints as tiny programs embedded inside of a larger * running Android application. Examples of this would be simple utilities to visualize and * edit {@link SharedPreferences} data, kick off sync or other background tasks, inject custom * data temporarily into the process for debugging/reproducibility, upload error reports, * etc. * <p> * See {@code ./scripts/dumpapp} for more information on how to use this system once * enabled. * * @param plugins The set of plugins to use. */ public InitializerBuilder enableDumpapp(DumperPluginsProvider plugins) { mDumperPlugins = Util.throwIfNull(plugins); return this; } public InitializerBuilder enableWebKitInspector(InspectorModulesProvider modules) { mInspectorModules = modules; return this; } public Initializer build() { return new BuilderBasedInitializer(this); } } private static class BuilderBasedInitializer extends Initializer { @Nullable private final DumperPluginsProvider mDumperPlugins; @Nullable private final InspectorModulesProvider mInspectorModules; private BuilderBasedInitializer(InitializerBuilder b) { super(b.mContext); mDumperPlugins = b.mDumperPlugins; mInspectorModules = b.mInspectorModules; } @Nullable @Override protected Iterable<DumperPlugin> getDumperPlugins() { return mDumperPlugins != null ? mDumperPlugins.get() : null; } @Nullable @Override protected Iterable<ChromeDevtoolsDomain> getInspectorModules() { return mInspectorModules != null ? mInspectorModules.get() : null; } } }