Repository: facebook/stetho Branch: main Commit: 2198797c0ff9 Files: 321 Total size: 1.1 MB Directory structure: gitextract_88n_xe_i/ ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── build-tools/ │ ├── protocol.json │ ├── readme.md │ └── scraper.js ├── build.gradle ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── release.gradle ├── scripts/ │ ├── .gitignore │ ├── dumpapp │ ├── hprof_dump.sh │ └── stetho_open.py ├── settings.gradle ├── stetho/ │ ├── .gitignore │ ├── build.gradle │ ├── gradle.properties │ ├── proguard-consumer.pro │ └── src/ │ ├── main/ │ │ ├── AndroidManifest.xml │ │ └── java/ │ │ └── com/ │ │ └── facebook/ │ │ └── stetho/ │ │ ├── DumperPluginsProvider.java │ │ ├── InspectorModulesProvider.java │ │ ├── Stetho.java │ │ ├── common/ │ │ │ ├── Accumulator.java │ │ │ ├── ArrayListAccumulator.java │ │ │ ├── ExceptionUtil.java │ │ │ ├── ListUtil.java │ │ │ ├── LogRedirector.java │ │ │ ├── LogUtil.java │ │ │ ├── Predicate.java │ │ │ ├── ProcessUtil.java │ │ │ ├── ReflectionUtil.java │ │ │ ├── StringUtil.java │ │ │ ├── ThreadBound.java │ │ │ ├── UncheckedCallable.java │ │ │ ├── Utf8Charset.java │ │ │ ├── Util.java │ │ │ └── android/ │ │ │ ├── AccessibilityUtil.java │ │ │ ├── DialogFragmentAccessor.java │ │ │ ├── FragmentAccessor.java │ │ │ ├── FragmentActivityAccessor.java │ │ │ ├── FragmentCompat.java │ │ │ ├── FragmentCompatFramework.java │ │ │ ├── FragmentCompatSupportLib.java │ │ │ ├── FragmentCompatUtil.java │ │ │ ├── FragmentManagerAccessor.java │ │ │ ├── HandlerUtil.java │ │ │ ├── ResourcesUtil.java │ │ │ ├── ViewGroupUtil.java │ │ │ └── ViewUtil.java │ │ ├── dumpapp/ │ │ │ ├── ArgsHelper.java │ │ │ ├── DumpException.java │ │ │ ├── DumpUsageException.java │ │ │ ├── DumpappFramingException.java │ │ │ ├── DumpappHttpSocketLikeHandler.java │ │ │ ├── DumpappOutputBrokenException.java │ │ │ ├── DumpappSocketLikeHandler.java │ │ │ ├── Dumper.java │ │ │ ├── DumperContext.java │ │ │ ├── DumperPlugin.java │ │ │ ├── Framer.java │ │ │ ├── GlobalOptions.java │ │ │ ├── UnexpectedFrameException.java │ │ │ └── plugins/ │ │ │ ├── CrashDumperPlugin.java │ │ │ ├── FilesDumperPlugin.java │ │ │ ├── HprofDumperPlugin.java │ │ │ └── SharedPreferencesDumperPlugin.java │ │ ├── inspector/ │ │ │ ├── ChromeDevtoolsServer.java │ │ │ ├── ChromeDiscoveryHandler.java │ │ │ ├── DevtoolsSocketHandler.java │ │ │ ├── MessageHandlingException.java │ │ │ ├── MethodDispatcher.java │ │ │ ├── MismatchedResponseException.java │ │ │ ├── console/ │ │ │ │ ├── CLog.java │ │ │ │ ├── ConsolePeerManager.java │ │ │ │ ├── RuntimeRepl.java │ │ │ │ └── RuntimeReplFactory.java │ │ │ ├── database/ │ │ │ │ ├── ContentProviderDatabaseDriver.java │ │ │ │ ├── ContentProviderSchema.java │ │ │ │ ├── DatabaseConnectionProvider.java │ │ │ │ ├── DatabaseDriver2Adapter.java │ │ │ │ ├── DatabaseFilesProvider.java │ │ │ │ ├── DefaultDatabaseConnectionProvider.java │ │ │ │ ├── DefaultDatabaseFilesProvider.java │ │ │ │ ├── SQLiteDatabaseCompat.java │ │ │ │ └── SqliteDatabaseDriver.java │ │ │ ├── domstorage/ │ │ │ │ ├── DOMStoragePeerManager.java │ │ │ │ └── SharedPreferencesHelper.java │ │ │ ├── elements/ │ │ │ │ ├── AbstractChainedDescriptor.java │ │ │ │ ├── AttributeAccumulator.java │ │ │ │ ├── ChainedDescriptor.java │ │ │ │ ├── ComputedStyleAccumulator.java │ │ │ │ ├── Descriptor.java │ │ │ │ ├── DescriptorMap.java │ │ │ │ ├── DescriptorProvider.java │ │ │ │ ├── DescriptorRegistrar.java │ │ │ │ ├── Document.java │ │ │ │ ├── DocumentProvider.java │ │ │ │ ├── DocumentProviderFactory.java │ │ │ │ ├── DocumentProviderListener.java │ │ │ │ ├── DocumentView.java │ │ │ │ ├── ElementInfo.java │ │ │ │ ├── NodeDescriptor.java │ │ │ │ ├── NodeType.java │ │ │ │ ├── ObjectDescriptor.java │ │ │ │ ├── Origin.java │ │ │ │ ├── ShadowDocument.java │ │ │ │ ├── StyleAccumulator.java │ │ │ │ ├── StyleRuleNameAccumulator.java │ │ │ │ └── android/ │ │ │ │ ├── AccessibilityNodeInfoWrapper.java │ │ │ │ ├── ActivityDescriptor.java │ │ │ │ ├── ActivityTracker.java │ │ │ │ ├── AndroidDescriptorHost.java │ │ │ │ ├── AndroidDocumentConstants.java │ │ │ │ ├── AndroidDocumentProvider.java │ │ │ │ ├── AndroidDocumentProviderFactory.java │ │ │ │ ├── AndroidDocumentRoot.java │ │ │ │ ├── ApplicationDescriptor.java │ │ │ │ ├── DialogDescriptor.java │ │ │ │ ├── DialogFragmentDescriptor.java │ │ │ │ ├── DocumentHiddenView.java │ │ │ │ ├── FragmentDescriptor.java │ │ │ │ ├── HighlightableDescriptor.java │ │ │ │ ├── MethodInvoker.java │ │ │ │ ├── TextViewDescriptor.java │ │ │ │ ├── ViewDescriptor.java │ │ │ │ ├── ViewGroupDescriptor.java │ │ │ │ ├── ViewHighlightOverlays.java │ │ │ │ ├── ViewHighlighter.java │ │ │ │ ├── WindowDescriptor.java │ │ │ │ └── window/ │ │ │ │ ├── WindowRootViewCompactV16Impl.java │ │ │ │ ├── WindowRootViewCompactV18Impl.java │ │ │ │ ├── WindowRootViewCompactV19Impl.java │ │ │ │ └── WindowRootViewCompat.java │ │ │ ├── helper/ │ │ │ │ ├── ChromePeerManager.java │ │ │ │ ├── IntegerFormatter.java │ │ │ │ ├── ObjectIdMapper.java │ │ │ │ ├── PeerRegistrationListener.java │ │ │ │ ├── PeersRegisteredListener.java │ │ │ │ └── ThreadBoundProxy.java │ │ │ ├── jsonrpc/ │ │ │ │ ├── DisconnectReceiver.java │ │ │ │ ├── JsonRpcException.java │ │ │ │ ├── JsonRpcPeer.java │ │ │ │ ├── JsonRpcResult.java │ │ │ │ ├── PendingRequest.java │ │ │ │ ├── PendingRequestCallback.java │ │ │ │ └── protocol/ │ │ │ │ ├── EmptyResult.java │ │ │ │ ├── JsonRpcError.java │ │ │ │ ├── JsonRpcEvent.java │ │ │ │ ├── JsonRpcRequest.java │ │ │ │ └── JsonRpcResponse.java │ │ │ ├── network/ │ │ │ │ ├── AsyncPrettyPrinter.java │ │ │ │ ├── AsyncPrettyPrinterExecutorHolder.java │ │ │ │ ├── AsyncPrettyPrinterFactory.java │ │ │ │ ├── AsyncPrettyPrinterInitializer.java │ │ │ │ ├── AsyncPrettyPrinterRegistry.java │ │ │ │ ├── CountingOutputStream.java │ │ │ │ ├── DecompressionHelper.java │ │ │ │ ├── DefaultResponseHandler.java │ │ │ │ ├── DownloadingAsyncPrettyPrinterFactory.java │ │ │ │ ├── GunzippingOutputStream.java │ │ │ │ ├── MimeMatcher.java │ │ │ │ ├── NetworkEventReporter.java │ │ │ │ ├── NetworkEventReporterImpl.java │ │ │ │ ├── NetworkPeerManager.java │ │ │ │ ├── PrettyPrinterDisplayType.java │ │ │ │ ├── RequestBodyHelper.java │ │ │ │ ├── ResourceTypeHelper.java │ │ │ │ ├── ResponseBodyData.java │ │ │ │ ├── ResponseBodyFileManager.java │ │ │ │ ├── ResponseHandler.java │ │ │ │ ├── ResponseHandlingInputStream.java │ │ │ │ ├── SimpleBinaryInspectorWebSocketFrame.java │ │ │ │ └── SimpleTextInspectorWebSocketFrame.java │ │ │ ├── protocol/ │ │ │ │ ├── ChromeDevtoolsDomain.java │ │ │ │ ├── ChromeDevtoolsMethod.java │ │ │ │ └── module/ │ │ │ │ ├── BaseDatabaseDriver.java │ │ │ │ ├── CSS.java │ │ │ │ ├── Console.java │ │ │ │ ├── DOM.java │ │ │ │ ├── DOMStorage.java │ │ │ │ ├── Database.java │ │ │ │ ├── DatabaseConstants.java │ │ │ │ ├── DatabaseDescriptor.java │ │ │ │ ├── DatabaseDriver2.java │ │ │ │ ├── Debugger.java │ │ │ │ ├── HeapProfiler.java │ │ │ │ ├── Inspector.java │ │ │ │ ├── Network.java │ │ │ │ ├── Page.java │ │ │ │ ├── Profiler.java │ │ │ │ ├── Runtime.java │ │ │ │ ├── SimpleBooleanResult.java │ │ │ │ └── Worker.java │ │ │ ├── runtime/ │ │ │ │ └── RhinoDetectingRuntimeReplFactory.java │ │ │ └── screencast/ │ │ │ └── ScreencastDispatcher.java │ │ ├── json/ │ │ │ ├── ObjectMapper.java │ │ │ └── annotation/ │ │ │ ├── JsonProperty.java │ │ │ └── JsonValue.java │ │ ├── server/ │ │ │ ├── AddressNameHelper.java │ │ │ ├── CompositeInputStream.java │ │ │ ├── LazySocketHandler.java │ │ │ ├── LeakyBufferedInputStream.java │ │ │ ├── LocalSocketServer.java │ │ │ ├── PeerAuthorizationException.java │ │ │ ├── ProtocolDetectingSocketHandler.java │ │ │ ├── SecureSocketHandler.java │ │ │ ├── ServerManager.java │ │ │ ├── SocketHandler.java │ │ │ ├── SocketHandlerFactory.java │ │ │ ├── SocketLike.java │ │ │ ├── SocketLikeHandler.java │ │ │ └── http/ │ │ │ ├── ExactPathMatcher.java │ │ │ ├── HandlerRegistry.java │ │ │ ├── HttpHandler.java │ │ │ ├── HttpHeaders.java │ │ │ ├── HttpStatus.java │ │ │ ├── LightHttpBody.java │ │ │ ├── LightHttpMessage.java │ │ │ ├── LightHttpRequest.java │ │ │ ├── LightHttpResponse.java │ │ │ ├── LightHttpServer.java │ │ │ ├── PathMatcher.java │ │ │ └── RegexpPathMatcher.java │ │ └── websocket/ │ │ ├── CloseCodes.java │ │ ├── Frame.java │ │ ├── FrameHelper.java │ │ ├── MaskingHelper.java │ │ ├── ReadCallback.java │ │ ├── ReadHandler.java │ │ ├── SimpleEndpoint.java │ │ ├── SimpleSession.java │ │ ├── WebSocketHandler.java │ │ ├── WebSocketSession.java │ │ ├── WriteCallback.java │ │ └── WriteHandler.java │ └── test/ │ └── java/ │ └── com/ │ └── facebook/ │ └── stetho/ │ ├── PluginBuilderTest.java │ ├── inspector/ │ │ ├── database/ │ │ │ └── DatabasePeerManagerTest.java │ │ ├── elements/ │ │ │ └── android/ │ │ │ ├── MethodInvokerTest.java │ │ │ └── ViewDescriptorTest.java │ │ └── network/ │ │ ├── AsyncPrettyPrintResponseBodyTest.java │ │ ├── GunzippingOutputStreamTest.java │ │ └── ResponseHandlingInputStreamTest.java │ └── json/ │ └── ObjectMapperTest.java ├── stetho-js-rhino/ │ ├── .gitignore │ ├── README.md │ ├── build.gradle │ ├── gradle.properties │ ├── proguard-consumer.pro │ └── src/ │ ├── main/ │ │ ├── AndroidManifest.xml │ │ └── java/ │ │ └── com/ │ │ └── facebook/ │ │ └── stetho/ │ │ └── rhino/ │ │ ├── JsConsole.java │ │ ├── JsFormat.java │ │ ├── JsRuntimeRepl.java │ │ └── JsRuntimeReplFactoryBuilder.java │ └── test/ │ └── java/ │ └── com/ │ └── facebook/ │ └── stetho/ │ └── rhino/ │ └── JsFormatTest.java ├── stetho-okhttp/ │ ├── .gitignore │ ├── build.gradle │ ├── gradle.properties │ └── src/ │ ├── main/ │ │ ├── AndroidManifest.xml │ │ └── java/ │ │ └── com/ │ │ └── facebook/ │ │ └── stetho/ │ │ └── okhttp/ │ │ └── StethoInterceptor.java │ └── test/ │ └── java/ │ └── com/ │ └── facebook/ │ └── stetho/ │ └── okhttp/ │ └── StethoInterceptorTest.java ├── stetho-okhttp3/ │ ├── .gitignore │ ├── build.gradle │ ├── gradle.properties │ └── src/ │ ├── main/ │ │ ├── AndroidManifest.xml │ │ └── java/ │ │ └── com/ │ │ └── facebook/ │ │ └── stetho/ │ │ └── okhttp3/ │ │ └── StethoInterceptor.java │ └── test/ │ └── java/ │ └── com/ │ └── facebook/ │ └── stetho/ │ └── okhttp3/ │ └── StethoInterceptorTest.java ├── stetho-sample/ │ ├── .gitignore │ ├── build.gradle │ └── src/ │ ├── debug/ │ │ ├── AndroidManifest.xml │ │ └── java/ │ │ └── com/ │ │ └── facebook/ │ │ └── stetho/ │ │ └── sample/ │ │ ├── APODDumperPlugin.java │ │ ├── HelloWorldDumperPlugin.java │ │ └── SampleDebugApplication.java │ └── main/ │ ├── AndroidManifest.xml │ ├── java/ │ │ └── com/ │ │ └── facebook/ │ │ └── stetho/ │ │ └── sample/ │ │ ├── APODActivity.java │ │ ├── APODContentProvider.java │ │ ├── APODContract.java │ │ ├── APODRssFetcher.java │ │ ├── Constants.java │ │ ├── HtmlScraper.java │ │ ├── IRCChatActivity.java │ │ ├── IRCClientConnection.java │ │ ├── IRCConnectActivity.java │ │ ├── MainActivity.java │ │ ├── Networker.java │ │ ├── SampleApplication.java │ │ └── SettingsActivity.java │ └── res/ │ ├── layout/ │ │ ├── apod_list_item.xml │ │ ├── dialog_layout.xml │ │ ├── irc_chat_activity.xml │ │ ├── irc_connect_activity.xml │ │ ├── irc_console_row.xml │ │ └── main_activity.xml │ ├── values/ │ │ ├── arrays.xml │ │ ├── dimens.xml │ │ └── strings.xml │ └── xml/ │ └── settings.xml ├── stetho-timber/ │ ├── .gitignore │ ├── build.gradle │ ├── gradle.properties │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ └── java/ │ └── com/ │ └── facebook/ │ └── stetho/ │ └── timber/ │ └── StethoTree.java └── stetho-urlconnection/ ├── .gitignore ├── build.gradle ├── gradle.properties └── src/ └── main/ ├── AndroidManifest.xml └── java/ └── com/ └── facebook/ └── stetho/ └── urlconnection/ ├── ByteArrayRequestEntity.java ├── SimpleRequestEntity.java ├── StethoURLConnectionManager.java ├── StethoURLConnectionManagerImpl.java ├── URLConnectionInspectorHeaders.java ├── URLConnectionInspectorRequest.java ├── URLConnectionInspectorResponse.java └── Util.java ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ .gradle /local.properties /.idea *.iml .DS_Store /build ================================================ FILE: .travis.yml ================================================ language: android jdk: - oraclejdk8 before_install: - yes | sdkmanager "platforms;android-27" android: components: - tools - android-28 - platform-tools branches: except: - gh-pages # Disable Travis container-based infrastructure to correct # resource consumption in gradle presumably due to dexing. sudo: true ================================================ FILE: CHANGELOG.md ================================================ Change Log ========== ## Version 1.6.0 _2021-03-17_ * Fix #662/665: AndroidX upgrade. ## Version 1.5.1 _2019-03-17_ * **Fix view inspection crash on Android P+** View highlighting was using a legacy pre-O API which was removed in P that caused a crash when highlighting a view in the Elements tab. This has now been fixed (#607) and the Elements tab is expected to work normally again. * Fix #325/295: Add ability to track windows in Elements tab (dialog, popups, etc) * Fix #614: Add the ability to manually control Runtime class object tracking (for J2V8 integration) * Fix #612/613: Fix issue subclassing Runtime class and other devtools domain modules * Fix #602: Make it possible to change welcome banner (see Page class constructor) * Fix #600: Actually make .remove() method work properly in DefaultDumperPluginsBuilder * Fix #596: Sort SharedPreferences entries * Fix #623: dumpapp now supports ADB_SERVER_SOCKET variable * Fix #589: Fix parsing issue using dumpapp script causing failure to connect * Fix #529: WebSocket session info now displayed correctly on upgrade * Fix #541/580: Avoid crash when installing the Stetho interceptor in the wrong okhttp chain (the request chain lacks a connection instance) ## Version 1.5.0 _2017_04_13_ * **Generic socket support** Added the ability to inspect arbitrary sockets via Chrome devtools' WebSocket APIs. This support is integrated into stetho-sample as a demo, and in future releases we'd like to make it available generically for at least okhttp customers. * **Major Elements tab extensibility points** Significant expansion to AndroidDocumentProviderFactory which allows for a much greater degree of customization, including the ability to have editable styles. Over time we'd like to incorporate this support into native Android UI but also to make it even easier to unlock powerful new customizations for custom view or entire rendering systems. * Fix #392: Fix database id mapping for multiple database providers * Fix #521: Fix hit testing for empty view groups * Fix #513: Fix LeakCanary false positive * Fix #514: Handle runtime exceptions from database drivers * Fix #511: Remove accidental hard dependency on support library * Fix #406: Fix stetho-rhino importClass/Package not working properly * Fix #499: Treat HttpURLConnection headers as case insensitive * Fix #512: Fix severe stdin issue in dumpapp script * Fix #486: Fix TextViewDescriptor NPE when text is null ## Version 1.4.2 _2016_12_14_ * **Bug fixes** Fixes a few bugs in the Elements tab. * Fix #381: Fixes NPE while rotating the device with retained fragments. * Fix #447: Support Instant Run in android studio, by fixing ObjectMapper * Fix #456: Support ANDROID_ADB_SERVER_PORT * Fix #454: Upgrade to OkHttp 3.4.2 * Fix #449: Make sure only unfocusable children's descriptions are being co-opted by parents ## Version 1.4.1 _2016_09_13_ * Fix #432 Have DefaultDatabaseProvider return filename v1.4.0 exposed a long standing bug relating to loading databases. ## Version 1.4.0 _2016_09_07_ * **Add UI Accessibility Properties to Styles tab** Added support for accessibility inspection, which allows users to select a View and see whether or not it will be focusable by an Accessibility Service, why it will or won't be focusable, the text description sent to Accessibility Services, and any AccessibilityActions that are currently available on the View. * Fix #367: Fixed SqliteDatabaseDriver with custom DatabaseFilesProvider * Fix #424: Make aar be the default packaging in maven ## Version 1.3.1 _2016-02-25_ * **Major performance improvements in Elements tab** Several performance, correctness, and stability improvements related to how Stetho performs tree diffing. * Fix #349: Fix dumpapp scripts under various edge cases. * Fix #357: Remove static fields from exported view "styles". ## Version 1.3.0 _2016-01-20_ * **okhttp3 support!** A new module, stetho-okhttp3, has been added which supports the new okhttp3 APIs. Note that stetho-okhttp is now deprecated. * **Removed Apache HttpClient dependency** A new, lightweight HTTP server implementation replaces it in Stetho and the dumpapp protocol has been modified to no longer use HTTP. Old dumpapp scripts will still work with new clients, however the opposite will hang! * **Custom database drivers** Completely custom or ContentProvider-based database drivers are available which allow greater inspection options with some configuration. See `DefaultInspectorModulesBuilder#provideDatabaseDriver`. * New #282: Show view margins in "Styles" subtab. * New #289: Show SQLite views as tables. * New #294: dumpapp now responds to `STETHO_PROCESS` env variable in addition to the `--process` flag. * Fix #286: Minor JsConsole improvements. * Fix #297: Sort CSS properties by name. * Fix #292: Minor JSON serialization fixes. * Fix #299: Memory leak fixes in view inspection (still some likely remain). * Fix #305: Add proguard rules to stetho-js-rhino aar artifact. * Fix #313: Work around issues formatting `Long.MIN_VALUE` and possibly others when showing in the database view. * Fix #332: NPE in ShadowDocument.getGarbageElements(). ## Version 1.2.0 _2015-09-11_ * **View properties support!** The "Styles" and "Computed" sub-tabs in "Elements" are now implemented, complete with the box model diagram and a summary of the most useful view properties. * **Screencasting** Click the small screen icon in the upper right to view a live preview of your phone's screen while using Stetho! Coming soon: mouse/keyboard support. * **Console tab support** Arbitrary Java/JavaScript support added to the Console with the optional `stetho-js-rhino` dependency. See [`stetho-sample/build.gradle`](stetho-sample/build.gradle) for details. * **New simpler initialization and customization API** Most callers can now just use `Stetho.initializeWithDefaults(context)`. * New #218: Ability to pass pretty printers for binary data in the Network tab. * New #248: Implement transparent request decompression. * New #225: Ability to search View hierarchy (invoke with CTRL+F on the Elements tab). * New #238: Add EXPLAIN support in SQL console. * New #222: Add PRAGMA support in SQL console. * New #207: Add `dumpapp files` plugin. * New #181: Highlight view margins and padding when hovering over DOM entry. * New #211: Implement DialogFragment in Elements tab. * Fix #231: Sort database and shared preferences entries by name. * Fix #206: Fix small memory leak in View hierarchy support. * Fix #204: Use DOM tree diffing to fix ListView and a number of other edge case view hierarchies. * Fix #183: Fix Fragment support in Elements tab. ## Version 1.1.1 _2015-05-01_ * **Updated patent grant!** See https://code.facebook.com/posts/1639473982937255/updating-our-open-source-patent-grant/ * New: `stetho-timber` added to redirect log messages to the Stetho console. * Fix #140: More efficient and simpler Fragment accessor code. * Fix #123: All view inspection features are now available for ICS (API 15) and up (some features required JB MR2, API 18). * Fix #154: Fix subtle race when a database is removed after the DevTools UI is opened. * Fix #151: Crash when rapidly adding/removing SharedPreferences keys. * Fix #142: View inspection "hit testing" didn't work as intended with its two-pass design. * Fix: Ignore extraneous files when WAL is enabled for SQLite databases. ## Version 1.1.0 _2015-04-02_ * **View inspection!** For ICS (API 15) and up, we now have full View inspection support in the "Elements" tab! Lots of goodies such as `` instances virtually placed in the hierarchy, view highlighting, and the ability to tap on a view to jump to its position in the hierarchy. Some features are only available for JB MR2 (API 18). * New #109: Add `dumpapp hprof` plugin to conveniently extract HPROF memory dumps. * New #110: Add `dumpapp crash` plugin to mechanize process death in a variety of ways. * New #105: Simplify excluding Stetho from release builds (exercised in `stetho-sample`). * New #40: Support SQLite databases in arbitrary directories. * Fix #115: Support multiple headers with the same name (most notably, HTTP cookies). * Fix #108: Workaround throughput issue in Android's LocalSocket#flush() method. * Fix #88: Avoid OOM on huge request/response HTTP payloads. * Fix #82: Provide visual feedback for INSERT/UPDATE/DELETE statements. * Fix: Javadoc JAR should now be uploaded properly to Maven. ## Version 1.0.1 _2015-02-27_ * **`SharedPreferences` inspection.** It's now possible to inspect SharedPreference files from the "Resources" tab. * New #65: Show non-default process name to chrome://inspect UI. * Fix #57: HTTP responses without the Content-Type header do not appear in the DevTools UI. * Fix #49: Unconditional "Could not bind to socket" error. * Fix #37: Duplicate dumpapp endpoints for the same process. * Fix: Use raw process name for Stetho sockets to fix an issue formatting choices in `dumpapp` ## Version 1.0.0 _2015-02-18_ * Initial release. ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Code of Conduct Facebook has adopted a Code of Conduct that we expect project participants to adhere to. Please read the [full text](https://code.fb.com/codeofconduct/) so that you can understand what actions will and will not be tolerated. ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing to Stetho We want to make contributing to this project as easy and transparent as possible. ## Our Development Process We work directly in the github project and provide versioned releases appropriate for major milestones and minor bug fixes or improvements. GitHub is used directly for issues and pull requests and the developers actively respond to requests. ## Pull Requests We actively welcome your pull requests. 1. Fork the repo and create your branch from `master`. 2. If you've added code that should be tested, add tests 3. If you've changed APIs, update the documentation. 4. Ensure the test suite passes. 5. Make sure your code lints. 6. If you haven't already, complete the Contributor License Agreement ("CLA"). ## Contributor License Agreement ("CLA") In order to accept your pull request, we need you to submit a CLA. You only need to do this once to work on any of Facebook's open source projects. Complete your CLA here: ## Issues We use GitHub issues to track public bugs. Please ensure your description is clear and has sufficient instructions to be able to reproduce the issue. Facebook has a [bounty program](https://www.facebook.com/whitehat/) for the safe disclosure of security bugs. In those cases, please go through the process outlined on that page and do not file a public issue. ## Coding Style * 2 spaces for indentation rather than tabs * Line wrapping indents 4 spaces * 100 character line length * One parameter per line when line wrapping is required * Use the `m` member variable prefix for private fields * Opening braces to appear on the same line as code ## License By contributing to Stetho, you agree that your contributions will be licensed under its MIT license. See LICENSE file for details. ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) Facebook, Inc. and its affiliates. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Stetho [![Build Status](https://travis-ci.org/facebook/stetho.svg?branch=master)](https://travis-ci.org/facebook/stetho) [Stetho](https://facebook.github.io/stetho) is a sophisticated debug bridge for Android applications. When enabled, developers have access to the Chrome Developer Tools feature natively part of the Chrome desktop browser. Developers can also choose to enable the optional `dumpapp` tool which offers a powerful command-line interface to application internals. Once you complete the set-up instructions below, just start your app and point your laptop browser to `chrome://inspect`. Click the "Inspect" button to begin. ## Set-up ### Download Download [the latest JARs](https://github.com/facebook/stetho/releases/latest) or grab via Gradle: ```groovy implementation 'com.facebook.stetho:stetho:1.6.0' ``` or Maven: ```xml com.facebook.stetho stetho 1.6.0 ``` Only the main `stetho` dependency is strictly required; however, you may also wish to use one of the network helpers: ```groovy implementation 'com.facebook.stetho:stetho-okhttp3:1.6.0' ``` or: ```groovy implementation 'com.facebook.stetho:stetho-urlconnection:1.6.0' ``` You can also enable a JavaScript console with: ```groovy implementation 'com.facebook.stetho:stetho-js-rhino:1.6.0' ``` For more details on how to customize the JavaScript runtime see [stetho-js-rhino](stetho-js-rhino/). ### Putting it together Integrating with Stetho is intended to be seamless and straightforward for most existing Android applications. There is a simple initialization step which occurs in your `Application` class: ```java public class MyApplication extends Application { public void onCreate() { super.onCreate(); Stetho.initializeWithDefaults(this); } } ``` Also ensure that your `MyApplication` Java class is registered in your `AndroidManifest.xml` file, otherwise you will not see an "Inspect" button in `chrome://inspect/#devices` : ```xml ``` This brings up most of the default configuration but does not enable some additional hooks (most notably, network inspection). See below for specific details on individual subsystems. ### Enable network inspection If you are using the popular [OkHttp](https://github.com/square/okhttp) library at the 3.x release, you can use the [Interceptors](https://github.com/square/okhttp/wiki/Interceptors) system to automatically hook into your existing stack. This is currently the simplest and most straightforward way to enable network inspection: ```java new OkHttpClient.Builder() .addNetworkInterceptor(new StethoInterceptor()) .build() ``` Note that okhttp 2.x will work as well, but with slightly different syntax and you must use the `stetho-okhttp` artifact (not `stetho-okhttp3`). As interceptors can modify the request and response, add the Stetho interceptor after all others to get an accurate view of the network traffic. If you are using `HttpURLConnection`, you can use `StethoURLConnectionManager` to assist with integration though you should be aware that there are some caveats with this approach. In particular, you must explicitly add `Accept-Encoding: gzip` to the request headers and manually handle compressed responses in order for Stetho to report compressed payload sizes. See the [`stetho-sample` project](stetho-sample) for more details. ## Going further ### Custom dumpapp plugins Custom plugins are the preferred means of extending the `dumpapp` system and can be added easily during configuration. Simply replace your configuration step as such: ```java Stetho.initialize(Stetho.newInitializerBuilder(context) .enableDumpapp(new DumperPluginsProvider() { @Override public Iterable get() { return new Stetho.DefaultDumperPluginsBuilder(context) .provide(new MyDumperPlugin()) .finish(); } }) .enableWebKitInspector(Stetho.defaultInspectorModulesProvider(context)) .build()) ``` See the [`stetho-sample` project](stetho-sample) for more details. ## Improve Stetho! See the [CONTRIBUTING.md](CONTRIBUTING.md) file for how to help out. ## License Stetho is MIT-licensed. See LICENSE file for more details. ================================================ FILE: build-tools/protocol.json ================================================ { "version": { "major": "1", "minor": "1" }, "domains": [{ "domain": "Inspector", "hidden": true, "types": [], "commands": [ { "name": "enable", "description": "Enables inspector domain notifications.", "handlers": ["browser", "renderer"] }, { "name": "disable", "description": "Disables inspector domain notifications." } ], "events": [ { "name": "evaluateForTestInFrontend", "parameters": [ { "name": "testCallId", "type": "integer" }, { "name": "script", "type": "string" } ] }, { "name": "inspect", "parameters": [ { "name": "object", "$ref": "Runtime.RemoteObject" }, { "name": "hints", "type": "object" } ] }, { "name": "detached", "description": "Fired when remote debugging connection is about to be terminated. Contains detach reason.", "parameters": [ { "name": "reason", "type": "string", "description": "The reason why connection has been terminated." } ], "handlers": ["browser"] }, { "name": "targetCrashed", "description": "Fired when debugging target has crashed", "handlers": ["browser"] } ] }, { "domain": "Memory", "hidden": true, "commands": [ { "name": "getDOMCounters", "returns": [ { "name": "documents", "type": "integer" }, { "name": "nodes", "type": "integer" }, { "name": "jsEventListeners", "type": "integer" } ] } ] }, { "domain": "Page", "description": "Actions and events related to the inspected page belong to the page domain.", "types": [ { "id": "ResourceType", "type": "string", "enum": ["Document", "Stylesheet", "Image", "Media", "Font", "Script", "TextTrack", "XHR", "WebSocket", "Other"], "description": "Resource type as it was perceived by the rendering engine." }, { "id": "FrameId", "type": "string", "description": "Unique frame identifier." }, { "id": "Frame", "type": "object", "description": "Information about the Frame on the page.", "properties": [ { "name": "id", "type": "string", "description": "Frame unique identifier." }, { "name": "parentId", "type": "string", "optional": true, "description": "Parent frame identifier." }, { "name": "loaderId", "$ref": "Network.LoaderId", "description": "Identifier of the loader associated with this frame." }, { "name": "name", "type": "string", "optional": true, "description": "Frame's name as specified in the tag." }, { "name": "url", "type": "string", "description": "Frame document's URL." }, { "name": "securityOrigin", "type": "string", "description": "Frame document's security origin." }, { "name": "mimeType", "type": "string", "description": "Frame document's mimeType as determined by the browser." } ] }, { "id": "FrameResourceTree", "type": "object", "description": "Information about the Frame hierarchy along with their cached resources.", "properties": [ { "name": "frame", "$ref": "Frame", "description": "Frame information for this tree item." }, { "name": "childFrames", "type": "array", "optional": true, "items": { "$ref": "FrameResourceTree" }, "description": "Child frames." }, { "name": "resources", "type": "array", "items": { "type": "object", "properties": [ { "name": "url", "type": "string", "description": "Resource URL." }, { "name": "type", "$ref": "ResourceType", "description": "Type of this resource." }, { "name": "mimeType", "type": "string", "description": "Resource mimeType as determined by the browser." }, { "name": "failed", "type": "boolean", "optional": true, "description": "True if the resource failed to load." }, { "name": "canceled", "type": "boolean", "optional": true, "description": "True if the resource was canceled during loading." } ] }, "description": "Information about frame resources." } ], "hidden": true }, { "id": "ScriptIdentifier", "type": "string", "description": "Unique script identifier.", "hidden": true }, { "id": "NavigationEntry", "type": "object", "description": "Navigation history entry.", "properties": [ { "name": "id", "type": "integer", "description": "Unique id of the navigation history entry." }, { "name": "url", "type": "string", "description": "URL of the navigation history entry." }, { "name": "title", "type": "string", "description": "Title of the navigation history entry." } ], "hidden": true }, { "id": "ScreencastFrameMetadata", "type": "object", "description": "Screencast frame metadata", "properties": [ { "name": "offsetTop", "type": "number", "hidden": true, "description": "Top offset in DIP." }, { "name": "pageScaleFactor", "type": "number", "hidden": true, "description": "Page scale factor." }, { "name": "deviceWidth", "type": "number", "hidden": true, "description": "Device screen width in DIP." }, { "name": "deviceHeight", "type": "number", "hidden": true, "description": "Device screen height in DIP." }, { "name": "scrollOffsetX", "type": "number", "hidden": true, "description": "Position of horizontal scroll in CSS pixels." }, { "name": "scrollOffsetY", "type": "number", "hidden": true, "description": "Position of vertical scroll in CSS pixels." }, { "name": "timestamp", "type": "number", "optional": true, "hidden": true, "description": "Frame swap timestamp." } ], "hidden": true }, { "id": "DialogType", "description": "Javascript dialog type", "type": "string", "enum": ["alert", "confirm", "prompt", "beforeunload"], "hidden": true } ], "commands": [ { "name": "enable", "description": "Enables page domain notifications.", "handlers": ["browser", "renderer"] }, { "name": "disable", "description": "Disables page domain notifications.", "handlers": ["browser", "renderer"] }, { "name": "addScriptToEvaluateOnLoad", "parameters": [ { "name": "scriptSource", "type": "string" } ], "returns": [ { "name": "identifier", "$ref": "ScriptIdentifier", "description": "Identifier of the added script." } ], "hidden": true }, { "name": "removeScriptToEvaluateOnLoad", "parameters": [ { "name": "identifier", "$ref": "ScriptIdentifier" } ], "hidden": true }, { "name": "reload", "parameters": [ { "name": "ignoreCache", "type": "boolean", "optional": true, "description": "If true, browser cache is ignored (as if the user pressed Shift+refresh)." }, { "name": "scriptToEvaluateOnLoad", "type": "string", "optional": true, "description": "If set, the script will be injected into all frames of the inspected page after reload." } ], "description": "Reloads given page optionally ignoring the cache.", "handlers": ["browser", "renderer"] }, { "name": "navigate", "parameters": [ { "name": "url", "type": "string", "description": "URL to navigate the page to." } ], "returns": [ { "name": "frameId", "$ref": "FrameId", "hidden": true, "description": "Frame id that will be navigated." } ], "description": "Navigates current page to the given URL.", "handlers": ["browser", "renderer"] }, { "name": "getNavigationHistory", "parameters": [], "returns": [ { "name": "currentIndex", "type": "integer", "description": "Index of the current navigation history entry." }, { "name": "entries", "type": "array", "items": { "$ref": "NavigationEntry" }, "description": "Array of navigation history entries." } ], "description": "Returns navigation history for the current page.", "hidden": true, "handlers": ["browser"] }, { "name": "navigateToHistoryEntry", "parameters": [ { "name": "entryId", "type": "integer", "description": "Unique id of the entry to navigate to." } ], "description": "Navigates current page to the given history entry.", "hidden": true, "handlers": ["browser"] }, { "name": "getCookies", "returns": [ { "name": "cookies", "type": "array", "items": { "$ref": "Network.Cookie" }, "description": "Array of cookie objects." } ], "description": "Returns all browser cookies. Depending on the backend support, will return detailed cookie information in the cookies field.", "handlers": ["browser"], "async": true, "hidden": true, "redirect": "Network" }, { "name": "deleteCookie", "parameters": [ { "name": "cookieName", "type": "string", "description": "Name of the cookie to remove." }, { "name": "url", "type": "string", "description": "URL to match cooke domain and path." } ], "description": "Deletes browser cookie with given name, domain and path.", "handlers": ["browser"], "async": true, "hidden": true, "redirect": "Network" }, { "name": "getResourceTree", "description": "Returns present frame / resource tree structure.", "returns": [ { "name": "frameTree", "$ref": "FrameResourceTree", "description": "Present frame / resource tree structure." } ], "hidden": true }, { "name": "getResourceContent", "async": true, "description": "Returns content of the given resource.", "parameters": [ { "name": "frameId", "$ref": "FrameId", "description": "Frame id to get resource for." }, { "name": "url", "type": "string", "description": "URL of the resource to get content for." } ], "returns": [ { "name": "content", "type": "string", "description": "Resource content." }, { "name": "base64Encoded", "type": "boolean", "description": "True, if content was served as base64." } ], "hidden": true }, { "name": "searchInResource", "description": "Searches for given string in resource content.", "parameters": [ { "name": "frameId", "$ref": "FrameId", "description": "Frame id for resource to search in." }, { "name": "url", "type": "string", "description": "URL of the resource to search in." }, { "name": "query", "type": "string", "description": "String to search for." }, { "name": "caseSensitive", "type": "boolean", "optional": true, "description": "If true, search is case sensitive." }, { "name": "isRegex", "type": "boolean", "optional": true, "description": "If true, treats string parameter as regex." } ], "returns": [ { "name": "result", "type": "array", "items": { "$ref": "Debugger.SearchMatch" }, "description": "List of search matches." } ], "hidden": true }, { "name": "setDocumentContent", "description": "Sets given markup as the document's HTML.", "parameters": [ { "name": "frameId", "$ref": "FrameId", "description": "Frame id to set HTML for." }, { "name": "html", "type": "string", "description": "HTML content to set." } ], "hidden": true }, { "name": "setDeviceMetricsOverride", "description": "Overrides the values of device screen dimensions (window.screen.width, window.screen.height, window.innerWidth, window.innerHeight, and \"device-width\"/\"device-height\"-related CSS media query results).", "parameters": [ { "name": "width", "type": "integer", "description": "Overriding width value in pixels (minimum 0, maximum 10000000). 0 disables the override." }, { "name": "height", "type": "integer", "description": "Overriding height value in pixels (minimum 0, maximum 10000000). 0 disables the override." }, { "name": "deviceScaleFactor", "type": "number", "description": "Overriding device scale factor value. 0 disables the override." }, { "name": "mobile", "type": "boolean", "description": "Whether to emulate mobile device. This includes viewport meta tag, overlay scrollbars, text autosizing and more." }, { "name": "fitWindow", "type": "boolean", "description": "Whether a view that exceeds the available browser window area should be scaled down to fit." }, { "name": "scale", "type": "number", "optional": true, "description": "Scale to apply to resulting view image. Ignored in |fitWindow| mode." }, { "name": "offsetX", "type": "number", "optional": true, "description": "X offset to shift resulting view image by. Ignored in |fitWindow| mode." }, { "name": "offsetY", "type": "number", "optional": true, "description": "Y offset to shift resulting view image by. Ignored in |fitWindow| mode." }, { "name": "screenWidth", "type": "integer", "optional": true, "description": "Overriding screen width value in pixels (minimum 0, maximum 10000000). Only used for |mobile==true|." }, { "name": "screenHeight", "type": "integer", "optional": true, "description": "Overriding screen height value in pixels (minimum 0, maximum 10000000). Only used for |mobile==true|." }, { "name": "positionX", "type": "integer", "optional": true, "description": "Overriding view X position on screen in pixels (minimum 0, maximum 10000000). Only used for |mobile==true|." }, { "name": "positionY", "type": "integer", "optional": true, "description": "Overriding view Y position on screen in pixels (minimum 0, maximum 10000000). Only used for |mobile==true|." } ], "handlers": ["browser"], "redirect": "Emulation", "hidden": true }, { "name": "clearDeviceMetricsOverride", "description": "Clears the overriden device metrics.", "handlers": ["browser"], "redirect": "Emulation", "hidden": true }, { "name": "setGeolocationOverride", "description": "Overrides the Geolocation Position or Error. Omitting any of the parameters emulates position unavailable.", "parameters": [ { "name": "latitude", "type": "number", "optional": true, "description": "Mock latitude"}, { "name": "longitude", "type": "number", "optional": true, "description": "Mock longitude"}, { "name": "accuracy", "type": "number", "optional": true, "description": "Mock accuracy"} ], "redirect": "Emulation", "handlers": ["browser"] }, { "name": "clearGeolocationOverride", "description": "Clears the overriden Geolocation Position and Error.", "redirect": "Emulation", "handlers": ["browser"] }, { "name": "setDeviceOrientationOverride", "description": "Overrides the Device Orientation.", "parameters": [ { "name": "alpha", "type": "number", "description": "Mock alpha"}, { "name": "beta", "type": "number", "description": "Mock beta"}, { "name": "gamma", "type": "number", "description": "Mock gamma"} ], "redirect": "DeviceOrientation", "hidden": true }, { "name": "clearDeviceOrientationOverride", "description": "Clears the overridden Device Orientation.", "redirect": "DeviceOrientation", "hidden": true }, { "name": "setTouchEmulationEnabled", "parameters": [ { "name": "enabled", "type": "boolean", "description": "Whether the touch event emulation should be enabled." }, { "name": "configuration", "type": "string", "enum": ["mobile", "desktop"], "optional": true, "description": "Touch/gesture events configuration. Default: current platform." } ], "description": "Toggles mouse event-based touch event emulation.", "hidden": true, "redirect": "Emulation", "handlers": ["browser", "renderer"] }, { "name": "captureScreenshot", "async": true, "description": "Capture page screenshot.", "parameters": [], "returns": [ { "name": "data", "type": "string", "description": "Base64-encoded image data (PNG)." } ], "hidden": true, "handlers": ["browser"] }, { "name": "canScreencast", "description": "Tells whether screencast is supported.", "returns": [ { "name": "result", "type": "boolean", "description": "True if screencast is supported." } ], "hidden": true, "handlers": ["browser"] }, { "name": "startScreencast", "description": "Starts sending each frame using the screencastFrame event.", "parameters": [ { "name": "format", "type": "string", "optional": true, "enum": ["jpeg", "png"], "description": "Image compression format." }, { "name": "quality", "type": "integer", "optional": true, "description": "Compression quality from range [0..100]." }, { "name": "maxWidth", "type": "integer", "optional": true, "description": "Maximum screenshot width." }, { "name": "maxHeight", "type": "integer", "optional": true, "description": "Maximum screenshot height." } ], "hidden": true, "handlers": ["browser", "renderer"] }, { "name": "stopScreencast", "description": "Stops sending each frame in the screencastFrame.", "hidden": true, "handlers": ["browser", "renderer"] }, { "name": "screencastFrameAck", "description": "Acknowledges that a screencast frame has been received by the frontend.", "parameters": [ { "name": "frameNumber", "type": "integer", "description": "Frame number." } ], "hidden": true, "handlers": ["browser"] }, { "name": "handleJavaScriptDialog", "description": "Accepts or dismisses a JavaScript initiated dialog (alert, confirm, prompt, or onbeforeunload).", "parameters": [ { "name": "accept", "type": "boolean", "description": "Whether to accept or dismiss the dialog." }, { "name": "promptText", "type": "string", "optional": true, "description": "The text to enter into the dialog prompt before accepting. Used only if this is a prompt dialog." } ], "hidden": true, "handlers": ["browser"] }, { "name": "setShowViewportSizeOnResize", "description": "Paints viewport size upon main frame resize.", "parameters": [ { "name": "show", "type": "boolean", "description": "Whether to paint size or not." }, { "name": "showGrid", "type": "boolean", "optional": true, "description": "Whether to paint grid as well." } ], "hidden": true }, { "name": "setColorPickerEnabled", "parameters": [ { "name": "enabled", "type": "boolean", "description": "Shows / hides color picker" } ], "description": "Shows / hides color picker", "hidden": true, "handlers": ["browser"] }, { "name": "setOverlayMessage", "parameters": [ { "name": "message", "type": "string", "optional": true, "description": "Overlay message to display when paused in debugger." } ], "hidden": true, "description": "Sets overlay message." } ], "events": [ { "name": "domContentEventFired", "parameters": [ { "name": "timestamp", "type": "number" } ] }, { "name": "loadEventFired", "parameters": [ { "name": "timestamp", "type": "number" } ] }, { "name": "frameAttached", "description": "Fired when frame has been attached to its parent.", "parameters": [ { "name": "frameId", "$ref": "FrameId", "description": "Id of the frame that has been attached." }, { "name": "parentFrameId", "$ref": "FrameId", "description": "Parent frame identifier." } ] }, { "name": "frameNavigated", "description": "Fired once navigation of the frame has completed. Frame is now associated with the new loader.", "parameters": [ { "name": "frame", "$ref": "Frame", "description": "Frame object." } ] }, { "name": "frameDetached", "description": "Fired when frame has been detached from its parent.", "parameters": [ { "name": "frameId", "$ref": "FrameId", "description": "Id of the frame that has been detached." } ] }, { "name": "frameStartedLoading", "description": "Fired when frame has started loading.", "parameters": [ { "name": "frameId", "$ref": "FrameId", "description": "Id of the frame that has started loading." } ], "hidden": true }, { "name": "frameStoppedLoading", "description": "Fired when frame has stopped loading.", "parameters": [ { "name": "frameId", "$ref": "FrameId", "description": "Id of the frame that has stopped loading." } ], "hidden": true }, { "name": "frameScheduledNavigation", "description": "Fired when frame schedules a potential navigation.", "parameters": [ { "name": "frameId", "$ref": "FrameId", "description": "Id of the frame that has scheduled a navigation." }, { "name": "delay", "type": "number", "description": "Delay (in seconds) until the navigation is scheduled to begin. The navigation is not guaranteed to start." } ], "hidden": true }, { "name": "frameClearedScheduledNavigation", "description": "Fired when frame no longer has a scheduled navigation.", "parameters": [ { "name": "frameId", "$ref": "FrameId", "description": "Id of the frame that has cleared its scheduled navigation." } ], "hidden": true }, { "name": "frameResized", "hidden": true }, { "name": "javascriptDialogOpening", "description": "Fired when a JavaScript initiated dialog (alert, confirm, prompt, or onbeforeunload) is about to open.", "parameters": [ { "name": "message", "type": "string", "description": "Message that will be displayed by the dialog." }, { "name": "type", "$ref": "DialogType", "description": "Dialog type." } ], "hidden": true }, { "name": "javascriptDialogClosed", "description": "Fired when a JavaScript initiated dialog (alert, confirm, prompt, or onbeforeunload) has been closed.", "parameters": [ { "name": "result", "type": "boolean", "description": "Whether dialog was confirmed." } ], "hidden": true }, { "name": "screencastFrame", "description": "Compressed image data requested by the startScreencast.", "parameters": [ { "name": "data", "type": "string", "description": "Base64-encoded compressed image." }, { "name": "metadata", "$ref": "ScreencastFrameMetadata", "description": "Screencast frame metadata."}, { "name": "frameNumber", "type": "integer", "optional": true, "description": "Frame number."} ], "hidden": true, "handlers": ["browser"] }, { "name": "screencastVisibilityChanged", "description": "Fired when the page with currently enabled screencast was shown or hidden .", "parameters": [ { "name": "visible", "type": "boolean", "description": "True if the page is visible." } ], "hidden": true, "handlers": ["browser"] }, { "name": "colorPicked", "description": "Fired when a color has been picked.", "parameters": [ { "name": "color", "$ref": "DOM.RGBA", "description": "RGBA of the picked color." } ], "hidden": true, "handlers": ["browser"] }, { "name": "interstitialShown", "description": "Fired when interstitial page was shown", "hidden": true, "handlers": ["browser"] }, { "name": "interstitialHidden", "description": "Fired when interstitial page was hidden", "hidden": true, "handlers": ["browser"] } ] }, { "domain": "Rendering", "description": "This domain allows to control rendering of the page.", "hidden": true, "commands": [ { "name": "setShowPaintRects", "description": "Requests that backend shows paint rectangles", "parameters": [ { "name": "result", "type": "boolean", "description": "True for showing paint rectangles" } ] }, { "name": "setShowDebugBorders", "description": "Requests that backend shows debug borders on layers", "parameters": [ { "name": "show", "type": "boolean", "description": "True for showing debug borders" } ] }, { "name": "setShowFPSCounter", "description": "Requests that backend shows the FPS counter", "parameters": [ { "name": "show", "type": "boolean", "description": "True for showing the FPS counter" } ] }, { "name": "setContinuousPaintingEnabled", "description": "Requests that backend enables continuous painting", "parameters": [ { "name": "enabled", "type": "boolean", "description": "True for enabling cointinuous painting" } ] }, { "name": "setShowScrollBottleneckRects", "description": "Requests that backend shows scroll bottleneck rects", "parameters": [ { "name": "show", "type": "boolean", "description": "True for showing scroll bottleneck rects" } ] } ] }, { "domain": "Emulation", "description": "This domain emulates different environments for the page.", "hidden": true, "types": [ { "id": "Viewport", "type": "object", "description": "Visible page viewport", "properties": [ { "name": "scrollX", "type": "number", "description": "X scroll offset in CSS pixels." }, { "name": "scrollY", "type": "number", "description": "Y scroll offset in CSS pixels." }, { "name": "contentsWidth", "type": "number", "description": "Contents width in CSS pixels." }, { "name": "contentsHeight", "type": "number", "description": "Contents height in CSS pixels." }, { "name": "pageScaleFactor", "type": "number", "description": "Page scale factor." }, { "name": "minimumPageScaleFactor", "type": "number", "description": "Minimum page scale factor." }, { "name": "maximumPageScaleFactor", "type": "number", "description": "Maximum page scale factor." } ] } ], "commands": [ { "name": "setDeviceMetricsOverride", "description": "Overrides the values of device screen dimensions (window.screen.width, window.screen.height, window.innerWidth, window.innerHeight, and \"device-width\"/\"device-height\"-related CSS media query results).", "parameters": [ { "name": "width", "type": "integer", "description": "Overriding width value in pixels (minimum 0, maximum 10000000). 0 disables the override." }, { "name": "height", "type": "integer", "description": "Overriding height value in pixels (minimum 0, maximum 10000000). 0 disables the override." }, { "name": "deviceScaleFactor", "type": "number", "description": "Overriding device scale factor value. 0 disables the override." }, { "name": "mobile", "type": "boolean", "description": "Whether to emulate mobile device. This includes viewport meta tag, overlay scrollbars, text autosizing and more." }, { "name": "fitWindow", "type": "boolean", "description": "Whether a view that exceeds the available browser window area should be scaled down to fit." }, { "name": "scale", "type": "number", "optional": true, "description": "Scale to apply to resulting view image. Ignored in |fitWindow| mode." }, { "name": "offsetX", "type": "number", "optional": true, "description": "X offset to shift resulting view image by. Ignored in |fitWindow| mode." }, { "name": "offsetY", "type": "number", "optional": true, "description": "Y offset to shift resulting view image by. Ignored in |fitWindow| mode." }, { "name": "screenWidth", "type": "integer", "optional": true, "description": "Overriding screen width value in pixels (minimum 0, maximum 10000000). Only used for |mobile==true|." }, { "name": "screenHeight", "type": "integer", "optional": true, "description": "Overriding screen height value in pixels (minimum 0, maximum 10000000). Only used for |mobile==true|." }, { "name": "positionX", "type": "integer", "optional": true, "description": "Overriding view X position on screen in pixels (minimum 0, maximum 10000000). Only used for |mobile==true|." }, { "name": "positionY", "type": "integer", "optional": true, "description": "Overriding view Y position on screen in pixels (minimum 0, maximum 10000000). Only used for |mobile==true|." } ], "handlers": ["browser"] }, { "name": "clearDeviceMetricsOverride", "description": "Clears the overriden device metrics.", "handlers": ["browser"] }, { "name": "resetScrollAndPageScaleFactor", "description": "Requests that scroll offsets and page scale factor are reset to initial values." }, { "name": "setPageScaleFactor", "description": "Sets a specified page scale factor.", "parameters": [ { "name": "pageScaleFactor", "type": "number", "description": "Page scale factor." } ] }, { "name": "setScriptExecutionDisabled", "description": "Switches script execution in the page.", "parameters": [ { "name": "value", "type": "boolean", "description": "Whether script execution should be disabled in the page." } ] }, { "name": "setGeolocationOverride", "description": "Overrides the Geolocation Position or Error. Omitting any of the parameters emulates position unavailable.", "parameters": [ { "name": "latitude", "type": "number", "optional": true, "description": "Mock latitude"}, { "name": "longitude", "type": "number", "optional": true, "description": "Mock longitude"}, { "name": "accuracy", "type": "number", "optional": true, "description": "Mock accuracy"} ], "handlers": ["browser"] }, { "name": "clearGeolocationOverride", "description": "Clears the overriden Geolocation Position and Error.", "handlers": ["browser"] }, { "name": "setTouchEmulationEnabled", "parameters": [ { "name": "enabled", "type": "boolean", "description": "Whether the touch event emulation should be enabled." }, { "name": "configuration", "type": "string", "enum": ["mobile", "desktop"], "optional": true, "description": "Touch/gesture events configuration. Default: current platform." } ], "description": "Toggles mouse event-based touch event emulation.", "handlers": ["browser", "renderer"] }, { "name": "setEmulatedMedia", "parameters": [ { "name": "media", "type": "string", "description": "Media type to emulate. Empty string disables the override." } ], "description": "Emulates the given media for CSS media queries." }, { "name": "canEmulate", "description": "Tells whether emulation is supported.", "returns": [ { "name": "result", "type": "boolean", "description": "True if emulation is supported." } ], "handlers": ["browser"] } ], "events": [ { "name": "viewportChanged", "description": "Fired when a visible page viewport has changed. Only fired when device metrics are overridden.", "parameters": [ { "name": "viewport", "$ref": "Viewport", "description": "Viewport description." } ] } ] }, { "domain": "Runtime", "description": "Runtime domain exposes JavaScript runtime by means of remote evaluation and mirror objects. Evaluation results are returned as mirror object that expose object type, string representation and unique identifier that can be used for further object reference. Original objects are maintained in memory unless they are either explicitly released or are released along with the other objects in their object group.", "types": [ { "id": "RemoteObjectId", "type": "string", "description": "Unique object identifier." }, { "id": "RemoteObject", "type": "object", "description": "Mirror object referencing original JavaScript object.", "properties": [ { "name": "type", "type": "string", "enum": ["object", "function", "undefined", "string", "number", "boolean", "symbol"], "description": "Object type." }, { "name": "subtype", "type": "string", "optional": true, "enum": ["array", "null", "node", "regexp", "date", "map", "set", "iterator", "generator", "error"], "description": "Object subtype hint. Specified for object type values only." }, { "name": "className", "type": "string", "optional": true, "description": "Object class (constructor) name. Specified for object type values only." }, { "name": "value", "type": "any", "optional": true, "description": "Remote object value in case of primitive values or JSON values (if it was requested), or description string if the value can not be JSON-stringified (like NaN, Infinity, -Infinity, -0)." }, { "name": "description", "type": "string", "optional": true, "description": "String representation of the object." }, { "name": "objectId", "$ref": "RemoteObjectId", "optional": true, "description": "Unique object identifier (for non-primitive values)." }, { "name": "preview", "$ref": "ObjectPreview", "optional": true, "description": "Preview containing abbreviated property values. Specified for object type values only.", "hidden": true }, { "name": "customPreview", "$ref": "CustomPreview", "optional": true, "hidden": true} ] }, { "id": "CustomPreview", "type": "object", "hidden": true, "properties": [ { "name": "header", "type": "string"}, { "name": "hasBody", "type": "boolean"}, {"name": "formatterObjectId", "$ref": "RemoteObjectId"}, {"name": "configObjectId", "$ref": "RemoteObjectId", "optional": true} ] }, { "id": "ObjectPreview", "type": "object", "hidden": true, "description": "Object containing abbreviated remote object value.", "properties": [ { "name": "type", "type": "string", "enum": ["object", "function", "undefined", "string", "number", "boolean", "symbol"], "description": "Object type." }, { "name": "subtype", "type": "string", "optional": true, "enum": ["array", "null", "node", "regexp", "date", "map", "set", "iterator", "generator", "error"], "description": "Object subtype hint. Specified for object type values only." }, { "name": "description", "type": "string", "optional": true, "description": "String representation of the object." }, { "name": "lossless", "type": "boolean", "description": "Determines whether preview is lossless (contains all information of the original object)." }, { "name": "overflow", "type": "boolean", "description": "True iff some of the properties or entries of the original object did not fit." }, { "name": "properties", "type": "array", "items": { "$ref": "PropertyPreview" }, "description": "List of the properties." }, { "name": "entries", "type": "array", "items": { "$ref": "EntryPreview" }, "optional": true, "description": "List of the entries. Specified for map and set subtype values only." } ] }, { "id": "PropertyPreview", "type": "object", "hidden": true, "properties": [ { "name": "name", "type": "string", "description": "Property name." }, { "name": "type", "type": "string", "enum": ["object", "function", "undefined", "string", "number", "boolean", "symbol", "accessor"], "description": "Object type. Accessor means that the property itself is an accessor property." }, { "name": "value", "type": "string", "optional": true, "description": "User-friendly property value string." }, { "name": "valuePreview", "$ref": "ObjectPreview", "optional": true, "description": "Nested value preview." }, { "name": "subtype", "type": "string", "optional": true, "enum": ["array", "null", "node", "regexp", "date", "map", "set", "iterator", "generator", "error"], "description": "Object subtype hint. Specified for object type values only." } ] }, { "id": "EntryPreview", "type": "object", "hidden": true, "properties": [ { "name": "key", "$ref": "ObjectPreview", "optional": true, "description": "Preview of the key. Specified for map-like collection entries." }, { "name": "value", "$ref": "ObjectPreview", "description": "Preview of the value." } ] }, { "id": "PropertyDescriptor", "type": "object", "description": "Object property descriptor.", "properties": [ { "name": "name", "type": "string", "description": "Property name or symbol description." }, { "name": "value", "$ref": "RemoteObject", "optional": true, "description": "The value associated with the property." }, { "name": "writable", "type": "boolean", "optional": true, "description": "True if the value associated with the property may be changed (data descriptors only)." }, { "name": "get", "$ref": "RemoteObject", "optional": true, "description": "A function which serves as a getter for the property, or undefined if there is no getter (accessor descriptors only)." }, { "name": "set", "$ref": "RemoteObject", "optional": true, "description": "A function which serves as a setter for the property, or undefined if there is no setter (accessor descriptors only)." }, { "name": "configurable", "type": "boolean", "description": "True if the type of this property descriptor may be changed and if the property may be deleted from the corresponding object." }, { "name": "enumerable", "type": "boolean", "description": "True if this property shows up during enumeration of the properties on the corresponding object." }, { "name": "wasThrown", "type": "boolean", "optional": true, "description": "True if the result was thrown during the evaluation." }, { "name": "isOwn", "optional": true, "type": "boolean", "description": "True if the property is owned for the object.", "hidden": true }, { "name": "symbol", "$ref": "RemoteObject", "optional": true, "description": "Property symbol object, if the property is of the symbol type.", "hidden": true } ] }, { "id": "InternalPropertyDescriptor", "type": "object", "description": "Object internal property descriptor. This property isn't normally visible in JavaScript code.", "properties": [ { "name": "name", "type": "string", "description": "Conventional property name." }, { "name": "value", "$ref": "RemoteObject", "optional": true, "description": "The value associated with the property." } ], "hidden": true }, { "id": "CallArgument", "type": "object", "description": "Represents function call argument. Either remote object id objectId or primitive value or neither of (for undefined) them should be specified.", "properties": [ { "name": "value", "type": "any", "optional": true, "description": "Primitive value, or description string if the value can not be JSON-stringified (like NaN, Infinity, -Infinity, -0)." }, { "name": "objectId", "$ref": "RemoteObjectId", "optional": true, "description": "Remote object handle." }, { "name": "type", "optional": true, "hidden": true, "type": "string", "enum": ["object", "function", "undefined", "string", "number", "boolean", "symbol"], "description": "Object type." } ] }, { "id": "ExecutionContextId", "type": "integer", "description": "Id of an execution context." }, { "id": "ExecutionContextDescription", "type": "object", "description": "Description of an isolated world.", "properties": [ { "name": "id", "$ref": "ExecutionContextId", "description": "Unique id of the execution context. It can be used to specify in which execution context script evaluation should be performed." }, { "name": "type", "type": "string", "optional": true, "description": "Context type. It is used e.g. to distinguish content scripts from web page script.", "hidden": true }, { "name": "origin", "type": "string", "description": "Execution context origin.", "hidden": true}, { "name": "name", "type": "string", "description": "Human readable name describing given context.", "hidden": true}, { "name": "frameId", "type": "string", "description": "Id of the owning frame. May be an empty string if the context is not associated with a frame." } ] } ], "commands": [ { "name": "evaluate", "parameters": [ { "name": "expression", "type": "string", "description": "Expression to evaluate." }, { "name": "objectGroup", "type": "string", "optional": true, "description": "Symbolic group name that can be used to release multiple objects." }, { "name": "includeCommandLineAPI", "type": "boolean", "optional": true, "description": "Determines whether Command Line API should be available during the evaluation.", "hidden": true }, { "name": "doNotPauseOnExceptionsAndMuteConsole", "type": "boolean", "optional": true, "description": "Specifies whether evaluation should stop on exceptions and mute console. Overrides setPauseOnException state.", "hidden": true }, { "name": "contextId", "$ref": "ExecutionContextId", "optional": true, "description": "Specifies in which isolated context to perform evaluation. Each content script lives in an isolated context and this parameter may be used to specify one of those contexts. If the parameter is omitted or 0 the evaluation will be performed in the context of the inspected page." }, { "name": "returnByValue", "type": "boolean", "optional": true, "description": "Whether the result is expected to be a JSON object that should be sent by value." }, { "name": "generatePreview", "type": "boolean", "optional": true, "hidden": true, "description": "Whether preview should be generated for the result." } ], "returns": [ { "name": "result", "$ref": "RemoteObject", "description": "Evaluation result." }, { "name": "wasThrown", "type": "boolean", "optional": true, "description": "True if the result was thrown during the evaluation." }, { "name": "exceptionDetails", "$ref": "Debugger.ExceptionDetails", "optional": true, "hidden": true, "description": "Exception details."} ], "description": "Evaluates expression on global object." }, { "name": "callFunctionOn", "parameters": [ { "name": "objectId", "$ref": "RemoteObjectId", "description": "Identifier of the object to call function on." }, { "name": "functionDeclaration", "type": "string", "description": "Declaration of the function to call." }, { "name": "arguments", "type": "array", "items": { "$ref": "CallArgument", "description": "Call argument." }, "optional": true, "description": "Call arguments. All call arguments must belong to the same JavaScript world as the target object." }, { "name": "doNotPauseOnExceptionsAndMuteConsole", "type": "boolean", "optional": true, "description": "Specifies whether function call should stop on exceptions and mute console. Overrides setPauseOnException state.", "hidden": true }, { "name": "returnByValue", "type": "boolean", "optional": true, "description": "Whether the result is expected to be a JSON object which should be sent by value." }, { "name": "generatePreview", "type": "boolean", "optional": true, "hidden": true, "description": "Whether preview should be generated for the result." } ], "returns": [ { "name": "result", "$ref": "RemoteObject", "description": "Call result." }, { "name": "wasThrown", "type": "boolean", "optional": true, "description": "True if the result was thrown during the evaluation." } ], "description": "Calls function with given declaration on the given object. Object group of the result is inherited from the target object." }, { "name": "getProperties", "parameters": [ { "name": "objectId", "$ref": "RemoteObjectId", "description": "Identifier of the object to return properties for." }, { "name": "ownProperties", "optional": true, "type": "boolean", "description": "If true, returns properties belonging only to the element itself, not to its prototype chain." }, { "name": "accessorPropertiesOnly", "optional": true, "type": "boolean", "description": "If true, returns accessor properties (with getter/setter) only; internal properties are not returned either.", "hidden": true }, { "name": "generatePreview", "type": "boolean", "optional": true, "hidden": true, "description": "Whether preview should be generated for the results." } ], "returns": [ { "name": "result", "type": "array", "items": { "$ref": "PropertyDescriptor" }, "description": "Object properties." }, { "name": "internalProperties", "optional": true, "type": "array", "items": { "$ref": "InternalPropertyDescriptor" }, "description": "Internal object properties (only of the element itself).", "hidden": true }, { "name": "exceptionDetails", "$ref": "Debugger.ExceptionDetails", "optional": true, "hidden": true, "description": "Exception details."} ], "description": "Returns properties of a given object. Object group of the result is inherited from the target object." }, { "name": "releaseObject", "parameters": [ { "name": "objectId", "$ref": "RemoteObjectId", "description": "Identifier of the object to release." } ], "description": "Releases remote object with given id." }, { "name": "releaseObjectGroup", "parameters": [ { "name": "objectGroup", "type": "string", "description": "Symbolic object group name." } ], "description": "Releases all remote objects that belong to a given group." }, { "name": "run", "hidden": true, "description": "Tells inspected instance(worker or page) that it can run in case it was started paused." }, { "name": "enable", "description": "Enables reporting of execution contexts creation by means of executionContextCreated event. When the reporting gets enabled the event will be sent immediately for each existing execution context." }, { "name": "disable", "hidden": true, "description": "Disables reporting of execution contexts creation." }, { "name": "isRunRequired", "returns": [ { "name": "result", "type": "boolean", "description": "True if the Runtime is in paused on start state." } ], "hidden": true }, { "name": "setCustomObjectFormatterEnabled", "parameters": [ { "name": "enabled", "type": "boolean" } ], "hidden": true } ], "events": [ { "name": "executionContextCreated", "parameters": [ { "name": "context", "$ref": "ExecutionContextDescription", "description": "A newly created execution contex." } ], "description": "Issued when new execution context is created." }, { "name": "executionContextDestroyed", "parameters": [ { "name": "executionContextId", "$ref": "ExecutionContextId", "description": "Id of the destroyed context" } ], "description": "Issued when execution context is destroyed." }, { "name": "executionContextsCleared", "description": "Issued when all executionContexts were cleared in browser" } ] }, { "domain": "Console", "description": "Console domain defines methods and events for interaction with the JavaScript console. Console collects messages created by means of the JavaScript Console API. One needs to enable this domain using enable command in order to start receiving the console messages. Browser collects messages issued while console domain is not enabled as well and reports them using messageAdded notification upon enabling.", "types": [ { "id": "Timestamp", "type": "number", "description": "Number of seconds since epoch.", "hidden": true }, { "id": "ConsoleMessage", "type": "object", "description": "Console message.", "properties": [ { "name": "source", "type": "string", "enum": ["xml", "javascript", "network", "console-api", "storage", "appcache", "rendering", "security", "other", "deprecation"], "description": "Message source." }, { "name": "level", "type": "string", "enum": ["log", "warning", "error", "debug", "info", "revokedError"], "description": "Message severity." }, { "name": "text", "type": "string", "description": "Message text." }, { "name": "type", "type": "string", "optional": true, "enum": ["log", "dir", "dirxml", "table", "trace", "clear", "startGroup", "startGroupCollapsed", "endGroup", "assert", "profile", "profileEnd"], "description": "Console message type." }, { "name": "scriptId", "type": "string", "optional": true, "description": "Script ID of the message origin." }, { "name": "url", "type": "string", "optional": true, "description": "URL of the message origin." }, { "name": "line", "type": "integer", "optional": true, "description": "Line number in the resource that generated this message." }, { "name": "column", "type": "integer", "optional": true, "description": "Column number in the resource that generated this message." }, { "name": "repeatCount", "type": "integer", "optional": true, "description": "Repeat count for repeated messages." }, { "name": "parameters", "type": "array", "items": { "$ref": "Runtime.RemoteObject" }, "optional": true, "description": "Message parameters in case of the formatted message." }, { "name": "stackTrace", "$ref": "StackTrace", "optional": true, "description": "JavaScript stack trace for assertions and error messages." }, { "name": "asyncStackTrace", "$ref": "AsyncStackTrace", "optional": true, "description": "Asynchronous JavaScript stack trace that preceded this message, if available.", "hidden": true }, { "name": "networkRequestId", "$ref": "Network.RequestId", "optional": true, "description": "Identifier of the network request associated with this message." }, { "name": "timestamp", "$ref": "Timestamp", "description": "Timestamp, when this message was fired.", "hidden": true }, { "name": "executionContextId", "$ref": "Runtime.ExecutionContextId", "optional": true, "description": "Identifier of the context where this message was created", "hidden": true }, { "name": "messageId", "type": "integer", "hidden": true, "optional": true, "description": "Message id." }, { "name": "relatedMessageId", "type": "integer", "hidden": true, "optional": true, "description": "Related message id." } ] }, { "id": "CallFrame", "type": "object", "description": "Stack entry for console errors and assertions.", "properties": [ { "name": "functionName", "type": "string", "description": "JavaScript function name." }, { "name": "scriptId", "type": "string", "description": "JavaScript script id." }, { "name": "url", "type": "string", "description": "JavaScript script name or url." }, { "name": "lineNumber", "type": "integer", "description": "JavaScript script line number." }, { "name": "columnNumber", "type": "integer", "description": "JavaScript script column number." } ] }, { "id": "StackTrace", "type": "array", "items": { "$ref": "CallFrame" }, "description": "Call frames for assertions or error messages." }, { "id": "AsyncStackTrace", "type": "object", "properties": [ { "name": "callFrames", "type": "array", "items": { "$ref": "CallFrame" }, "description": "Call frames of the stack trace." }, { "name": "description", "type": "string", "optional": true, "description": "String label of this stack trace. For async traces this may be a name of the function that initiated the async call." }, { "name": "asyncStackTrace", "$ref": "AsyncStackTrace", "optional": true, "description": "Next asynchronous stack trace, if any." } ], "description": "Asynchronous JavaScript call stack.", "hidden": true } ], "commands": [ { "name": "enable", "description": "Enables console domain, sends the messages collected so far to the client by means of the messageAdded notification." }, { "name": "disable", "description": "Disables console domain, prevents further console messages from being reported to the client." }, { "name": "clearMessages", "description": "Clears console messages collected in the browser." } ], "events": [ { "name": "messageAdded", "parameters": [ { "name": "message", "$ref": "ConsoleMessage", "description": "Console message that has been added." } ], "description": "Issued when new console message is added." }, { "name": "messageRepeatCountUpdated", "parameters": [ { "name": "count", "type": "integer", "description": "New repeat count value." }, { "name": "timestamp", "$ref": "Timestamp", "description": "Timestamp of most recent message in batch.", "hidden": true } ], "description": "Is not issued. Will be gone in the future versions of the protocol.", "deprecated": true }, { "name": "messagesCleared", "description": "Issued when console is cleared. This happens either upon clearMessages command or after page navigation." } ] }, { "domain": "Network", "description": "Network domain allows tracking network activities of the page. It exposes information about http, file, data and other requests and responses, their headers, bodies, timing, etc.", "types": [ { "id": "LoaderId", "type": "string", "description": "Unique loader identifier." }, { "id": "RequestId", "type": "string", "description": "Unique request identifier." }, { "id": "Timestamp", "type": "number", "description": "Number of seconds since epoch." }, { "id": "Headers", "type": "object", "description": "Request / response headers as keys / values of JSON object." }, { "id": "ResourceTiming", "type": "object", "description": "Timing information for the request.", "properties": [ { "name": "requestTime", "type": "number", "description": "Timing's requestTime is a baseline in seconds, while the other numbers are ticks in milliseconds relatively to this requestTime." }, { "name": "proxyStart", "type": "number", "description": "Started resolving proxy." }, { "name": "proxyEnd", "type": "number", "description": "Finished resolving proxy." }, { "name": "dnsStart", "type": "number", "description": "Started DNS address resolve." }, { "name": "dnsEnd", "type": "number", "description": "Finished DNS address resolve." }, { "name": "connectStart", "type": "number", "description": "Started connecting to the remote host." }, { "name": "connectEnd", "type": "number", "description": "Connected to the remote host." }, { "name": "sslStart", "type": "number", "description": "Started SSL handshake." }, { "name": "sslEnd", "type": "number", "description": "Finished SSL handshake." }, { "name": "workerStart", "type": "number", "description": "Started running ServiceWorker.", "hidden": true }, { "name": "workerReady", "type": "number", "description": "Finished Starting ServiceWorker.", "hidden": true }, { "name": "sendStart", "type": "number", "description": "Started sending request." }, { "name": "sendEnd", "type": "number", "description": "Finished sending request." }, { "name": "receiveHeadersEnd", "type": "number", "description": "Finished receiving response headers." } ] }, { "id": "Request", "type": "object", "description": "HTTP request data.", "properties": [ { "name": "url", "type": "string", "description": "Request URL." }, { "name": "method", "type": "string", "description": "HTTP request method." }, { "name": "headers", "$ref": "Headers", "description": "HTTP request headers." }, { "name": "postData", "type": "string", "optional": true, "description": "HTTP POST request data." } ] }, { "id": "Response", "type": "object", "description": "HTTP response data.", "properties": [ { "name": "url", "type": "string", "description": "Response URL. This URL can be different from CachedResource.url in case of redirect." }, { "name": "status", "type": "number", "description": "HTTP response status code." }, { "name": "statusText", "type": "string", "description": "HTTP response status text." }, { "name": "headers", "$ref": "Headers", "description": "HTTP response headers." }, { "name": "headersText", "type": "string", "optional": true, "description": "HTTP response headers text." }, { "name": "mimeType", "type": "string", "description": "Resource mimeType as determined by the browser." }, { "name": "requestHeaders", "$ref": "Headers", "optional": true, "description": "Refined HTTP request headers that were actually transmitted over the network." }, { "name": "requestHeadersText", "type": "string", "optional": true, "description": "HTTP request headers text." }, { "name": "connectionReused", "type": "boolean", "description": "Specifies whether physical connection was actually reused for this request." }, { "name": "connectionId", "type": "number", "description": "Physical connection id that was actually used for this request." }, { "name": "remoteIPAddress", "type": "string", "optional": true, "hidden": true, "description": "Remote IP address." }, { "name": "remotePort", "type": "integer", "optional": true, "hidden": true, "description": "Remote port."}, { "name": "fromDiskCache", "type": "boolean", "optional": true, "description": "Specifies that the request was served from the disk cache." }, { "name": "fromServiceWorker", "type": "boolean", "optional": true, "description": "Specifies that the request was served from the ServiceWorker." }, { "name": "encodedDataLength", "type": "number", "optional": false, "description": "Total number of bytes received for this request so far." }, { "name": "timing", "$ref": "ResourceTiming", "optional": true, "description": "Timing information for the given request." }, { "name": "protocol", "type": "string", "optional": true, "description": "Protocol used to fetch this request." } ] }, { "id": "WebSocketRequest", "type": "object", "description": "WebSocket request data.", "hidden": true, "properties": [ { "name": "headers", "$ref": "Headers", "description": "HTTP request headers." } ] }, { "id": "WebSocketResponse", "type": "object", "description": "WebSocket response data.", "hidden": true, "properties": [ { "name": "status", "type": "number", "description": "HTTP response status code." }, { "name": "statusText", "type": "string", "description": "HTTP response status text." }, { "name": "headers", "$ref": "Headers", "description": "HTTP response headers." }, { "name": "headersText", "type": "string", "optional": true, "description": "HTTP response headers text." }, { "name": "requestHeaders", "$ref": "Headers", "optional": true, "description": "HTTP request headers." }, { "name": "requestHeadersText", "type": "string", "optional": true, "description": "HTTP request headers text." } ] }, { "id": "WebSocketFrame", "type": "object", "description": "WebSocket frame data.", "hidden": true, "properties": [ { "name": "opcode", "type": "number", "description": "WebSocket frame opcode." }, { "name": "mask", "type": "boolean", "description": "WebSocke frame mask." }, { "name": "payloadData", "type": "string", "description": "WebSocke frame payload data." } ] }, { "id": "CachedResource", "type": "object", "description": "Information about the cached resource.", "properties": [ { "name": "url", "type": "string", "description": "Resource URL. This is the url of the original network request." }, { "name": "type", "$ref": "Page.ResourceType", "description": "Type of this resource." }, { "name": "response", "$ref": "Response", "optional": true, "description": "Cached response data." }, { "name": "bodySize", "type": "number", "description": "Cached response body size." } ] }, { "id": "Initiator", "type": "object", "description": "Information about the request initiator.", "properties": [ { "name": "type", "type": "string", "enum": ["parser", "script", "other"], "description": "Type of this initiator." }, { "name": "stackTrace", "$ref": "Console.StackTrace", "optional": true, "description": "Initiator JavaScript stack trace, set for Script only." }, { "name": "url", "type": "string", "optional": true, "description": "Initiator URL, set for Parser type only." }, { "name": "lineNumber", "type": "number", "optional": true, "description": "Initiator line number, set for Parser type only." }, { "name": "asyncStackTrace", "$ref": "Console.AsyncStackTrace", "optional": true, "description": "Initiator asynchronous JavaScript stack trace, if available.", "hidden": true } ] }, { "id": "Cookie", "type": "object", "description": "Cookie object", "properties": [ { "name": "name", "type": "string", "description": "Cookie name." }, { "name": "value", "type": "string", "description": "Cookie value." }, { "name": "domain", "type": "string", "description": "Cookie domain." }, { "name": "path", "type": "string", "description": "Cookie path." }, { "name": "expires", "type": "number", "description": "Cookie expires." }, { "name": "size", "type": "integer", "description": "Cookie size." }, { "name": "httpOnly", "type": "boolean", "description": "True if cookie is http-only." }, { "name": "secure", "type": "boolean", "description": "True if cookie is secure." }, { "name": "session", "type": "boolean", "description": "True in case of session cookie." } ], "hidden": true } ], "commands": [ { "name": "enable", "description": "Enables network tracking, network events will now be delivered to the client." }, { "name": "disable", "description": "Disables network tracking, prevents network events from being sent to the client." }, { "name": "setUserAgentOverride", "description": "Allows overriding user agent with the given string.", "parameters": [ { "name": "userAgent", "type": "string", "description": "User agent to use." } ] }, { "name": "setExtraHTTPHeaders", "description": "Specifies whether to always send extra HTTP headers with the requests from this page.", "parameters": [ { "name": "headers", "$ref": "Headers", "description": "Map with extra HTTP headers." } ] }, { "name": "getResponseBody", "async": true, "description": "Returns content served for the given request.", "parameters": [ { "name": "requestId", "$ref": "RequestId", "description": "Identifier of the network request to get content for." } ], "returns": [ { "name": "body", "type": "string", "description": "Response body." }, { "name": "base64Encoded", "type": "boolean", "description": "True, if content was sent as base64." } ] }, { "name": "replayXHR", "description": "This method sends a new XMLHttpRequest which is identical to the original one. The following parameters should be identical: method, url, async, request body, extra headers, withCredentials attribute, user, password.", "parameters": [ { "name": "requestId", "$ref": "RequestId", "description": "Identifier of XHR to replay." } ], "hidden": true }, { "name": "setMonitoringXHREnabled", "parameters": [ { "name": "enabled", "type": "boolean", "description": "Monitoring enabled state." } ], "description": "Toggles monitoring of XMLHttpRequest. If true, console will receive messages upon each XHR issued.", "hidden": true }, { "name": "canClearBrowserCache", "description": "Tells whether clearing browser cache is supported.", "returns": [ { "name": "result", "type": "boolean", "description": "True if browser cache can be cleared." } ] }, { "name": "clearBrowserCache", "description": "Clears browser cache.", "handlers": ["browser"] }, { "name": "canClearBrowserCookies", "description": "Tells whether clearing browser cookies is supported.", "returns": [ { "name": "result", "type": "boolean", "description": "True if browser cookies can be cleared." } ] }, { "name": "clearBrowserCookies", "description": "Clears browser cookies.", "handlers": ["browser"] }, { "name": "getCookies", "returns": [ { "name": "cookies", "type": "array", "items": { "$ref": "Cookie" }, "description": "Array of cookie objects." } ], "description": "Returns all browser cookies. Depending on the backend support, will return detailed cookie information in the cookies field.", "handlers": ["browser"], "async": true, "hidden": true }, { "name": "deleteCookie", "parameters": [ { "name": "cookieName", "type": "string", "description": "Name of the cookie to remove." }, { "name": "url", "type": "string", "description": "URL to match cooke domain and path." } ], "description": "Deletes browser cookie with given name, domain and path.", "handlers": ["browser"], "async": true, "hidden": true }, { "name": "canEmulateNetworkConditions", "description": "Tells whether emulation of network conditions is supported.", "returns": [ { "name": "result", "type": "boolean", "description": "True if emulation of network conditions is supported." } ], "hidden": true, "handlers": ["browser"] }, { "name": "emulateNetworkConditions", "description": "Activates emulation of network conditions.", "parameters": [ { "name": "offline", "type": "boolean", "description": "True to emulate internet disconnection." }, { "name": "latency", "type": "number", "description": "Additional latency (ms)." }, { "name": "downloadThroughput", "type": "number", "description": "Maximal aggregated download throughput." }, { "name": "uploadThroughput", "type": "number", "description": "Maximal aggregated upload throughput." } ], "hidden": true, "handlers": ["browser", "renderer"] }, { "name": "setCacheDisabled", "parameters": [ { "name": "cacheDisabled", "type": "boolean", "description": "Cache disabled state." } ], "description": "Toggles ignoring cache for each request. If true, cache will not be used." }, { "name": "setDataSizeLimitsForTest", "parameters": [ { "name": "maxTotalSize", "type": "integer", "description": "Maximum total buffer size." }, { "name": "maxResourceSize", "type": "integer", "description": "Maximum per-resource size." } ], "description": "For testing.", "hidden": true } ], "events": [ { "name": "requestWillBeSent", "description": "Fired when page is about to send HTTP request.", "parameters": [ { "name": "requestId", "$ref": "RequestId", "description": "Request identifier." }, { "name": "frameId", "$ref": "Page.FrameId", "description": "Frame identifier.", "hidden": true }, { "name": "loaderId", "$ref": "LoaderId", "description": "Loader identifier." }, { "name": "documentURL", "type": "string", "description": "URL of the document this request is loaded for." }, { "name": "request", "$ref": "Request", "description": "Request data." }, { "name": "timestamp", "$ref": "Timestamp", "description": "Timestamp." }, { "name": "wallTime", "$ref": "Timestamp", "hidden": true, "description": "UTC Timestamp." }, { "name": "initiator", "$ref": "Initiator", "description": "Request initiator." }, { "name": "redirectResponse", "optional": true, "$ref": "Response", "description": "Redirect response data." }, { "name": "type", "$ref": "Page.ResourceType", "optional": true, "hidden": true, "description": "Type of this resource." } ] }, { "name": "requestServedFromCache", "description": "Fired if request ended up loading from cache.", "parameters": [ { "name": "requestId", "$ref": "RequestId", "description": "Request identifier." } ] }, { "name": "responseReceived", "description": "Fired when HTTP response is available.", "parameters": [ { "name": "requestId", "$ref": "RequestId", "description": "Request identifier." }, { "name": "frameId", "$ref": "Page.FrameId", "description": "Frame identifier.", "hidden": true }, { "name": "loaderId", "$ref": "LoaderId", "description": "Loader identifier." }, { "name": "timestamp", "$ref": "Timestamp", "description": "Timestamp." }, { "name": "type", "$ref": "Page.ResourceType", "description": "Resource type." }, { "name": "response", "$ref": "Response", "description": "Response data." } ] }, { "name": "dataReceived", "description": "Fired when data chunk was received over the network.", "parameters": [ { "name": "requestId", "$ref": "RequestId", "description": "Request identifier." }, { "name": "timestamp", "$ref": "Timestamp", "description": "Timestamp." }, { "name": "dataLength", "type": "integer", "description": "Data chunk length." }, { "name": "encodedDataLength", "type": "integer", "description": "Actual bytes received (might be less than dataLength for compressed encodings)." } ] }, { "name": "loadingFinished", "description": "Fired when HTTP request has finished loading.", "parameters": [ { "name": "requestId", "$ref": "RequestId", "description": "Request identifier." }, { "name": "timestamp", "$ref": "Timestamp", "description": "Timestamp." }, { "name": "encodedDataLength", "type": "number", "description": "Total number of bytes received for this request." } ] }, { "name": "loadingFailed", "description": "Fired when HTTP request has failed to load.", "parameters": [ { "name": "requestId", "$ref": "RequestId", "description": "Request identifier." }, { "name": "timestamp", "$ref": "Timestamp", "description": "Timestamp." }, { "name": "type", "$ref": "Page.ResourceType", "description": "Resource type." }, { "name": "errorText", "type": "string", "description": "User friendly error message." }, { "name": "canceled", "type": "boolean", "optional": true, "description": "True if loading was canceled." } ] }, { "name": "webSocketWillSendHandshakeRequest", "description": "Fired when WebSocket is about to initiate handshake.", "parameters": [ { "name": "requestId", "$ref": "RequestId", "description": "Request identifier." }, { "name": "timestamp", "$ref": "Timestamp", "description": "Timestamp." }, { "name": "wallTime", "$ref": "Timestamp", "hidden": true, "description": "UTC Timestamp." }, { "name": "request", "$ref": "WebSocketRequest", "description": "WebSocket request data." } ], "hidden": true }, { "name": "webSocketHandshakeResponseReceived", "description": "Fired when WebSocket handshake response becomes available.", "parameters": [ { "name": "requestId", "$ref": "RequestId", "description": "Request identifier." }, { "name": "timestamp", "$ref": "Timestamp", "description": "Timestamp." }, { "name": "response", "$ref": "WebSocketResponse", "description": "WebSocket response data." } ], "hidden": true }, { "name": "webSocketCreated", "description": "Fired upon WebSocket creation.", "parameters": [ { "name": "requestId", "$ref": "RequestId", "description": "Request identifier." }, { "name": "url", "type": "string", "description": "WebSocket request URL." } ], "hidden": true }, { "name": "webSocketClosed", "description": "Fired when WebSocket is closed.", "parameters": [ { "name": "requestId", "$ref": "RequestId", "description": "Request identifier." }, { "name": "timestamp", "$ref": "Timestamp", "description": "Timestamp." } ], "hidden": true }, { "name": "webSocketFrameReceived", "description": "Fired when WebSocket frame is received.", "parameters": [ { "name": "requestId", "$ref": "RequestId", "description": "Request identifier." }, { "name": "timestamp", "$ref": "Timestamp", "description": "Timestamp." }, { "name": "response", "$ref": "WebSocketFrame", "description": "WebSocket response data." } ], "hidden": true }, { "name": "webSocketFrameError", "description": "Fired when WebSocket frame error occurs.", "parameters": [ { "name": "requestId", "$ref": "RequestId", "description": "Request identifier." }, { "name": "timestamp", "$ref": "Timestamp", "description": "Timestamp." }, { "name": "errorMessage", "type": "string", "description": "WebSocket frame error message." } ], "hidden": true }, { "name": "webSocketFrameSent", "description": "Fired when WebSocket frame is sent.", "parameters": [ { "name": "requestId", "$ref": "RequestId", "description": "Request identifier." }, { "name": "timestamp", "$ref": "Timestamp", "description": "Timestamp." }, { "name": "response", "$ref": "WebSocketFrame", "description": "WebSocket response data." } ], "hidden": true }, { "name": "eventSourceMessageReceived", "description": "Fired when EventSource message is received.", "parameters": [ { "name": "requestId", "$ref": "RequestId", "description": "Request identifier." }, { "name": "timestamp", "$ref": "Timestamp", "description": "Timestamp." }, { "name": "eventName", "type": "string", "description": "Message type." }, { "name": "eventId", "type": "string", "description": "Message identifier." }, { "name": "data", "type": "string", "description": "Message content." } ], "hidden": true } ] }, { "domain": "Database", "hidden": true, "types": [ { "id": "DatabaseId", "type": "string", "description": "Unique identifier of Database object.", "hidden": true }, { "id": "Database", "type": "object", "description": "Database object.", "hidden": true, "properties": [ { "name": "id", "$ref": "DatabaseId", "description": "Database ID." }, { "name": "domain", "type": "string", "description": "Database domain." }, { "name": "name", "type": "string", "description": "Database name." }, { "name": "version", "type": "string", "description": "Database version." } ] }, { "id": "Error", "type": "object", "description": "Database error.", "properties": [ { "name": "message", "type": "string", "description": "Error message." }, { "name": "code", "type": "integer", "description": "Error code." } ] } ], "commands": [ { "name": "enable", "description": "Enables database tracking, database events will now be delivered to the client." }, { "name": "disable", "description": "Disables database tracking, prevents database events from being sent to the client." }, { "name": "getDatabaseTableNames", "parameters": [ { "name": "databaseId", "$ref": "DatabaseId" } ], "returns": [ { "name": "tableNames", "type": "array", "items": { "type": "string" } } ] }, { "name": "executeSQL", "async": true, "parameters": [ { "name": "databaseId", "$ref": "DatabaseId" }, { "name": "query", "type": "string" } ], "returns": [ { "name": "columnNames", "type": "array", "optional": true, "items": { "type": "string" } }, { "name": "values", "type": "array", "optional": true, "items": { "type": "any" }}, { "name": "sqlError", "$ref": "Error", "optional": true } ] } ], "events": [ { "name": "addDatabase", "parameters": [ { "name": "database", "$ref": "Database" } ] } ] }, { "domain": "IndexedDB", "hidden": true, "types": [ { "id": "DatabaseWithObjectStores", "type": "object", "description": "Database with an array of object stores.", "properties": [ { "name": "name", "type": "string", "description": "Database name." }, { "name": "version", "type": "string", "description": "Deprecated string database version." }, { "name": "intVersion", "type": "integer", "description": "Integer database version." }, { "name": "objectStores", "type": "array", "items": { "$ref": "ObjectStore" }, "description": "Object stores in this database." } ] }, { "id": "ObjectStore", "type": "object", "description": "Object store.", "properties": [ { "name": "name", "type": "string", "description": "Object store name." }, { "name": "keyPath", "$ref": "KeyPath", "description": "Object store key path." }, { "name": "autoIncrement", "type": "boolean", "description": "If true, object store has auto increment flag set." }, { "name": "indexes", "type": "array", "items": { "$ref": "ObjectStoreIndex" }, "description": "Indexes in this object store." } ] }, { "id": "ObjectStoreIndex", "type": "object", "description": "Object store index.", "properties": [ { "name": "name", "type": "string", "description": "Index name." }, { "name": "keyPath", "$ref": "KeyPath", "description": "Index key path." }, { "name": "unique", "type": "boolean", "description": "If true, index is unique." }, { "name": "multiEntry", "type": "boolean", "description": "If true, index allows multiple entries for a key." } ] }, { "id": "Key", "type": "object", "description": "Key.", "properties": [ { "name": "type", "type": "string", "enum": ["number", "string", "date", "array"], "description": "Key type." }, { "name": "number", "type": "number", "optional": true, "description": "Number value." }, { "name": "string", "type": "string", "optional": true, "description": "String value." }, { "name": "date", "type": "number", "optional": true, "description": "Date value." }, { "name": "array", "type": "array", "optional": true, "items": { "$ref": "Key" }, "description": "Array value." } ] }, { "id": "KeyRange", "type": "object", "description": "Key range.", "properties": [ { "name": "lower", "$ref": "Key", "optional": true, "description": "Lower bound." }, { "name": "upper", "$ref": "Key", "optional": true, "description": "Upper bound." }, { "name": "lowerOpen", "type": "boolean", "description": "If true lower bound is open." }, { "name": "upperOpen", "type": "boolean", "description": "If true upper bound is open." } ] }, { "id": "DataEntry", "type": "object", "description": "Data entry.", "properties": [ { "name": "key", "type": "string", "description": "JSON-stringified key object." }, { "name": "primaryKey", "type": "string", "description": "JSON-stringified primary key object." }, { "name": "value", "type": "string", "description": "JSON-stringified value object." } ] }, { "id": "KeyPath", "type": "object", "description": "Key path.", "properties": [ { "name": "type", "type": "string", "enum": ["null", "string", "array"], "description": "Key path type." }, { "name": "string", "type": "string", "optional": true, "description": "String value." }, { "name": "array", "type": "array", "optional": true, "items": { "type": "string" }, "description": "Array value." } ] } ], "commands": [ { "name": "enable", "description": "Enables events from backend." }, { "name": "disable", "description": "Disables events from backend." }, { "name": "requestDatabaseNames", "async": true, "parameters": [ { "name": "securityOrigin", "type": "string", "description": "Security origin." } ], "returns": [ { "name": "databaseNames", "type": "array", "items": { "type": "string" }, "description": "Database names for origin." } ], "description": "Requests database names for given security origin." }, { "name": "requestDatabase", "async": true, "parameters": [ { "name": "securityOrigin", "type": "string", "description": "Security origin." }, { "name": "databaseName", "type": "string", "description": "Database name." } ], "returns": [ { "name": "databaseWithObjectStores", "$ref": "DatabaseWithObjectStores", "description": "Database with an array of object stores." } ], "description": "Requests database with given name in given frame." }, { "name": "requestData", "async": true, "parameters": [ { "name": "securityOrigin", "type": "string", "description": "Security origin." }, { "name": "databaseName", "type": "string", "description": "Database name." }, { "name": "objectStoreName", "type": "string", "description": "Object store name." }, { "name": "indexName", "type": "string", "description": "Index name, empty string for object store data requests." }, { "name": "skipCount", "type": "integer", "description": "Number of records to skip." }, { "name": "pageSize", "type": "integer", "description": "Number of records to fetch." }, { "name": "keyRange", "$ref": "KeyRange", "optional": true, "description": "Key range." } ], "returns": [ { "name": "objectStoreDataEntries", "type": "array", "items": { "$ref": "DataEntry" }, "description": "Array of object store data entries." }, { "name": "hasMore", "type": "boolean", "description": "If true, there are more entries to fetch in the given range." } ], "description": "Requests data from object store or index." }, { "name": "clearObjectStore", "async": true, "parameters": [ { "name": "securityOrigin", "type": "string", "description": "Security origin." }, { "name": "databaseName", "type": "string", "description": "Database name." }, { "name": "objectStoreName", "type": "string", "description": "Object store name." } ], "returns": [ ], "description": "Clears all entries from an object store." } ] }, { "domain": "CacheStorage", "hidden": true, "types": [ { "id": "CacheId", "type": "string", "description": "Unique identifier of the Cache object." }, { "id": "DataEntry", "type": "object", "description": "Data entry.", "properties": [ { "name": "request", "type": "string", "description": "Request url spec." }, { "name": "response", "type": "string", "description": "Response stataus text." } ] }, { "id": "Cache", "type": "object", "description": "Cache identifier.", "properties": [ { "name": "cacheId", "$ref": "CacheId", "description": "An opaque unique id of the cache." }, { "name": "securityOrigin", "type": "string", "description": "Security origin of the cache." }, { "name": "cacheName", "type": "string", "description": "The name of the cache." } ] } ], "commands": [ { "name": "requestCacheNames", "async": true, "parameters": [ { "name": "securityOrigin", "type": "string", "description": "Security origin." } ], "returns": [ { "name": "caches", "type": "array", "items": { "$ref": "Cache" }, "description": "Caches for the security origin." } ], "description": "Requests cache names." }, { "name": "requestEntries", "async": true, "parameters": [ { "name": "cacheId", "$ref": "CacheId", "description": "ID of cache to get entries from." }, { "name": "skipCount", "type": "integer", "description": "Number of records to skip." }, { "name": "pageSize", "type": "integer", "description": "Number of records to fetch." } ], "returns": [ { "name": "cacheDataEntries", "type": "array", "items": { "$ref": "DataEntry" }, "description": "Array of object store data entries." }, { "name": "hasMore", "type": "boolean", "description": "If true, there are more entries to fetch in the given range." } ], "description": "Requests data from cache." }, { "name": "deleteCache", "async": true, "parameters": [ { "name": "cacheId", "$ref": "CacheId", "description": "Id of cache for deletion." } ], "description": "Deletes a cache." }, { "name": "deleteEntry", "async": true, "parameters": [ { "name": "cacheId", "$ref": "CacheId", "description": "Id of cache where the entry will be deleted." }, { "name": "request", "type": "string", "description": "URL spec of the request." } ], "description": "Deletes a cache entry." } ] }, { "domain": "DOMStorage", "hidden": true, "description": "Query and modify DOM storage.", "types": [ { "id": "StorageId", "type": "object", "description": "DOM Storage identifier.", "hidden": true, "properties": [ { "name": "securityOrigin", "type": "string", "description": "Security origin for the storage." }, { "name": "isLocalStorage", "type": "boolean", "description": "Whether the storage is local storage (not session storage)." } ] }, { "id": "Item", "type": "array", "description": "DOM Storage item.", "hidden": true, "items": { "type": "string" } } ], "commands": [ { "name": "enable", "description": "Enables storage tracking, storage events will now be delivered to the client." }, { "name": "disable", "description": "Disables storage tracking, prevents storage events from being sent to the client." }, { "name": "getDOMStorageItems", "parameters": [ { "name": "storageId", "$ref": "StorageId" } ], "returns": [ { "name": "entries", "type": "array", "items": { "$ref": "Item" } } ] }, { "name": "setDOMStorageItem", "parameters": [ { "name": "storageId", "$ref": "StorageId" }, { "name": "key", "type": "string" }, { "name": "value", "type": "string" } ] }, { "name": "removeDOMStorageItem", "parameters": [ { "name": "storageId", "$ref": "StorageId" }, { "name": "key", "type": "string" } ] } ], "events": [ { "name": "domStorageItemsCleared", "parameters": [ { "name": "storageId", "$ref": "StorageId" } ] }, { "name": "domStorageItemRemoved", "parameters": [ { "name": "storageId", "$ref": "StorageId" }, { "name": "key", "type": "string" } ] }, { "name": "domStorageItemAdded", "parameters": [ { "name": "storageId", "$ref": "StorageId" }, { "name": "key", "type": "string" }, { "name": "newValue", "type": "string" } ] }, { "name": "domStorageItemUpdated", "parameters": [ { "name": "storageId", "$ref": "StorageId" }, { "name": "key", "type": "string" }, { "name": "oldValue", "type": "string" }, { "name": "newValue", "type": "string" } ] } ] }, { "domain": "ApplicationCache", "hidden": true, "types": [ { "id": "ApplicationCacheResource", "type": "object", "description": "Detailed application cache resource information.", "properties": [ { "name": "url", "type": "string", "description": "Resource url." }, { "name": "size", "type": "integer", "description": "Resource size." }, { "name": "type", "type": "string", "description": "Resource type." } ] }, { "id": "ApplicationCache", "type": "object", "description": "Detailed application cache information.", "properties": [ { "name": "manifestURL", "type": "string", "description": "Manifest URL." }, { "name": "size", "type": "number", "description": "Application cache size." }, { "name": "creationTime", "type": "number", "description": "Application cache creation time." }, { "name": "updateTime", "type": "number", "description": "Application cache update time." }, { "name": "resources", "type": "array", "items": { "$ref": "ApplicationCacheResource" }, "description": "Application cache resources." } ] }, { "id": "FrameWithManifest", "type": "object", "description": "Frame identifier - manifest URL pair.", "properties": [ { "name": "frameId", "$ref": "Page.FrameId", "description": "Frame identifier." }, { "name": "manifestURL", "type": "string", "description": "Manifest URL." }, { "name": "status", "type": "integer", "description": "Application cache status." } ] } ], "commands": [ { "name": "getFramesWithManifests", "returns": [ { "name": "frameIds", "type": "array", "items": { "$ref": "FrameWithManifest" }, "description": "Array of frame identifiers with manifest urls for each frame containing a document associated with some application cache." } ], "description": "Returns array of frame identifiers with manifest urls for each frame containing a document associated with some application cache." }, { "name": "enable", "description": "Enables application cache domain notifications." }, { "name": "getManifestForFrame", "parameters": [ { "name": "frameId", "$ref": "Page.FrameId", "description": "Identifier of the frame containing document whose manifest is retrieved." } ], "returns": [ { "name": "manifestURL", "type": "string", "description": "Manifest URL for document in the given frame." } ], "description": "Returns manifest URL for document in the given frame." }, { "name": "getApplicationCacheForFrame", "parameters": [ { "name": "frameId", "$ref": "Page.FrameId", "description": "Identifier of the frame containing document whose application cache is retrieved." } ], "returns": [ { "name": "applicationCache", "$ref": "ApplicationCache", "description": "Relevant application cache data for the document in given frame." } ], "description": "Returns relevant application cache data for the document in given frame." } ], "events": [ { "name": "applicationCacheStatusUpdated", "parameters": [ { "name": "frameId", "$ref": "Page.FrameId", "description": "Identifier of the frame containing document whose application cache updated status." }, { "name": "manifestURL", "type": "string", "description": "Manifest URL." }, { "name": "status", "type": "integer", "description": "Updated application cache status." } ] }, { "name": "networkStateUpdated", "parameters": [ { "name": "isNowOnline", "type": "boolean" } ] } ] }, { "domain": "FileSystem", "hidden": true, "types": [ { "id": "Entry", "type": "object", "properties": [ { "name": "url", "type": "string", "description": "filesystem: URL for the entry." }, { "name": "name", "type": "string", "description": "The name of the file or directory." }, { "name": "isDirectory", "type": "boolean", "description": "True if the entry is a directory." }, { "name": "mimeType", "type": "string", "optional": true, "description": "MIME type of the entry, available for a file only." }, { "name": "resourceType", "$ref": "Page.ResourceType", "optional": true, "description": "ResourceType of the entry, available for a file only." }, { "name": "isTextFile", "type": "boolean", "optional": true, "description": "True if the entry is a text file." } ], "description": "Represents a browser side file or directory." }, { "id": "Metadata", "type": "object", "properties": [ { "name": "modificationTime", "type": "number", "description": "Modification time." }, { "name": "size", "type": "number", "description": "File size. This field is always zero for directories." } ], "description": "Represents metadata of a file or entry." } ], "commands": [ { "name": "enable", "description": "Enables events from backend." }, { "name": "disable", "description": "Disables events from backend." }, { "name": "requestFileSystemRoot", "async": true, "parameters": [ { "name": "origin", "type": "string", "description": "Security origin of requesting FileSystem. One of frames in current page needs to have this security origin." }, { "name": "type", "type": "string", "enum": ["temporary", "persistent"], "description": "FileSystem type of requesting FileSystem." } ], "returns": [ { "name": "errorCode", "type": "integer", "description": "0, if no error. Otherwise, errorCode is set to FileError::ErrorCode value." }, { "name": "root", "$ref": "Entry", "optional": true, "description": "Contains root of the requested FileSystem if the command completed successfully." } ], "description": "Returns root directory of the FileSystem, if exists." }, { "name": "requestDirectoryContent", "async": true, "parameters": [ { "name": "url", "type": "string", "description": "URL of the directory that the frontend is requesting to read from." } ], "returns": [ { "name": "errorCode", "type": "integer", "description": "0, if no error. Otherwise, errorCode is set to FileError::ErrorCode value." }, { "name": "entries", "type": "array", "items": { "$ref": "Entry" }, "optional": true, "description": "Contains all entries on directory if the command completed successfully." } ], "description": "Returns content of the directory." }, { "name": "requestMetadata", "async": true, "parameters": [ { "name": "url", "type": "string", "description": "URL of the entry that the frontend is requesting to get metadata from." } ], "returns": [ { "name": "errorCode", "type": "integer", "description": "0, if no error. Otherwise, errorCode is set to FileError::ErrorCode value." }, { "name": "metadata", "$ref": "Metadata", "optional": true, "description": "Contains metadata of the entry if the command completed successfully." } ], "description": "Returns metadata of the entry." }, { "name": "requestFileContent", "async": true, "parameters": [ { "name": "url", "type": "string", "description": "URL of the file that the frontend is requesting to read from." }, { "name": "readAsText", "type": "boolean", "description": "True if the content should be read as text, otherwise the result will be returned as base64 encoded text." }, { "name": "start", "type": "integer", "optional": true, "description": "Specifies the start of range to read." }, { "name": "end", "type": "integer", "optional": true, "description": "Specifies the end of range to read exclusively." }, { "name": "charset", "type": "string", "optional": true, "description": "Overrides charset of the content when content is served as text." } ], "returns": [ { "name": "errorCode", "type": "integer", "description": "0, if no error. Otherwise, errorCode is set to FileError::ErrorCode value." }, { "name": "content", "type": "string", "optional": true, "description": "Content of the file." }, { "name": "charset", "type": "string", "optional": true, "description": "Charset of the content if it is served as text." } ], "description": "Returns content of the file. Result should be sliced into [start, end)." }, { "name": "deleteEntry", "async": true, "parameters": [ { "name": "url", "type": "string", "description": "URL of the entry to delete." } ], "returns": [ { "name": "errorCode", "type": "integer", "description": "0, if no error. Otherwise errorCode is set to FileError::ErrorCode value." } ], "description": "Deletes specified entry. If the entry is a directory, the agent deletes children recursively." } ] }, { "domain": "DOM", "description": "This domain exposes DOM read/write operations. Each DOM Node is represented with its mirror object that has an id. This id can be used to get additional information on the Node, resolve it into the JavaScript object wrapper, etc. It is important that client receives DOM events only for the nodes that are known to the client. Backend keeps track of the nodes that were sent to the client and never sends the same node twice. It is client's responsibility to collect information about the nodes that were sent to the client.

Note that iframe owner elements will return corresponding document elements as their child nodes.

", "types": [ { "id": "NodeId", "type": "integer", "description": "Unique DOM node identifier." }, { "id": "BackendNodeId", "type": "integer", "description": "Unique DOM node identifier used to reference a node that may not have been pushed to the front-end.", "hidden": true }, { "id": "BackendNode", "type": "object", "properties": [ { "name": "nodeType", "type": "integer", "description": "Node's nodeType." }, { "name": "nodeName", "type": "string", "description": "Node's nodeName." }, { "name": "backendNodeId", "$ref": "BackendNodeId" } ], "hidden": true, "description": "Backend node with a friendly name." }, { "id": "PseudoType", "type": "string", "enum": [ "first-line", "first-letter", "before", "after", "backdrop", "selection", "first-line-inherited", "scrollbar", "scrollbar-thumb", "scrollbar-button", "scrollbar-track", "scrollbar-track-piece", "scrollbar-corner", "resizer", "input-list-button" ], "description": "Pseudo element type." }, { "id": "ShadowRootType", "type": "string", "enum": ["user-agent", "author"], "description": "Shadow root type." }, { "id": "Node", "type": "object", "properties": [ { "name": "nodeId", "$ref": "NodeId", "description": "Node identifier that is passed into the rest of the DOM messages as the nodeId. Backend will only push node with given id once. It is aware of all requested nodes and will only fire DOM events for nodes known to the client." }, { "name": "nodeType", "type": "integer", "description": "Node's nodeType." }, { "name": "nodeName", "type": "string", "description": "Node's nodeName." }, { "name": "localName", "type": "string", "description": "Node's localName." }, { "name": "nodeValue", "type": "string", "description": "Node's nodeValue." }, { "name": "childNodeCount", "type": "integer", "optional": true, "description": "Child count for Container nodes." }, { "name": "children", "type": "array", "optional": true, "items": { "$ref": "Node" }, "description": "Child nodes of this node when requested with children." }, { "name": "attributes", "type": "array", "optional": true, "items": { "type": "string" }, "description": "Attributes of the Element node in the form of flat array [name1, value1, name2, value2]." }, { "name": "documentURL", "type": "string", "optional": true, "description": "Document URL that Document or FrameOwner node points to." }, { "name": "baseURL", "type": "string", "optional": true, "description": "Base URL that Document or FrameOwner node uses for URL completion.", "hidden": true }, { "name": "publicId", "type": "string", "optional": true, "description": "DocumentType's publicId." }, { "name": "systemId", "type": "string", "optional": true, "description": "DocumentType's systemId." }, { "name": "internalSubset", "type": "string", "optional": true, "description": "DocumentType's internalSubset." }, { "name": "xmlVersion", "type": "string", "optional": true, "description": "Document's XML version in case of XML documents." }, { "name": "name", "type": "string", "optional": true, "description": "Attr's name." }, { "name": "value", "type": "string", "optional": true, "description": "Attr's value." }, { "name": "pseudoType", "$ref": "PseudoType", "optional": true, "description": "Pseudo element type for this node." }, { "name": "shadowRootType", "$ref": "ShadowRootType", "optional": true, "description": "Shadow root type." }, { "name": "frameId", "$ref": "Page.FrameId", "optional": true, "description": "Frame ID for frame owner elements.", "hidden": true }, { "name": "contentDocument", "$ref": "Node", "optional": true, "description": "Content document for frame owner elements." }, { "name": "shadowRoots", "type": "array", "optional": true, "items": { "$ref": "Node" }, "description": "Shadow root list for given element host.", "hidden": true }, { "name": "templateContent", "$ref": "Node", "optional": true, "description": "Content document fragment for template elements.", "hidden": true }, { "name": "pseudoElements", "type": "array", "items": { "$ref": "Node" }, "optional": true, "description": "Pseudo elements associated with this node.", "hidden": true }, { "name": "importedDocument", "$ref": "Node", "optional": true, "description": "Import document for the HTMLImport links." }, { "name": "distributedNodes", "type": "array", "items": { "$ref": "BackendNode" }, "optional": true, "description": "Distributed nodes for given insertion point.", "hidden": true } ], "description": "DOM interaction is implemented in terms of mirror objects that represent the actual DOM nodes. DOMNode is a base node mirror type." }, { "id": "RGBA", "type": "object", "properties": [ { "name": "r", "type": "integer", "description": "The red component, in the [0-255] range." }, { "name": "g", "type": "integer", "description": "The green component, in the [0-255] range." }, { "name": "b", "type": "integer", "description": "The blue component, in the [0-255] range." }, { "name": "a", "type": "number", "optional": true, "description": "The alpha component, in the [0-1] range (default: 1)." } ], "description": "A structure holding an RGBA color." }, { "id": "Quad", "type": "array", "items": { "type": "number" }, "minItems": 8, "maxItems": 8, "description": "An array of quad vertices, x immediately followed by y for each point, points clock-wise.", "hidden": true }, { "id": "BoxModel", "type": "object", "hidden": true, "properties": [ { "name": "content", "$ref": "Quad", "description": "Content box" }, { "name": "padding", "$ref": "Quad", "description": "Padding box" }, { "name": "border", "$ref": "Quad", "description": "Border box" }, { "name": "margin", "$ref": "Quad", "description": "Margin box" }, { "name": "width", "type": "integer", "description": "Node width" }, { "name": "height", "type": "integer", "description": "Node height" }, { "name": "shapeOutside", "$ref": "ShapeOutsideInfo", "optional": true, "description": "Shape outside coordinates" } ], "description": "Box model." }, { "id": "ShapeOutsideInfo", "type": "object", "hidden": true, "properties": [ { "name": "bounds", "$ref": "Quad", "description": "Shape bounds" }, { "name": "shape", "type": "array", "items": { "type": "any"}, "description": "Shape coordinate details" }, { "name": "marginShape", "type": "array", "items": { "type": "any"}, "description": "Margin shape bounds" } ], "description": "CSS Shape Outside details." }, { "id": "Rect", "type": "object", "hidden": true, "properties": [ { "name": "x", "type": "number", "description": "X coordinate" }, { "name": "y", "type": "number", "description": "Y coordinate" }, { "name": "width", "type": "number", "description": "Rectangle width" }, { "name": "height", "type": "number", "description": "Rectangle height" } ], "description": "Rectangle." }, { "id": "HighlightConfig", "type": "object", "properties": [ { "name": "showInfo", "type": "boolean", "optional": true, "description": "Whether the node info tooltip should be shown (default: false)." }, { "name": "showRulers", "type": "boolean", "optional": true, "description": "Whether the rulers should be shown (default: false)." }, { "name": "showExtensionLines", "type": "boolean", "optional": true, "description": "Whether the extension lines from node to the rulers should be shown (default: false)." }, { "name": "showLayoutEditor", "type": "boolean", "optional": true, "hidden": true}, { "name": "contentColor", "$ref": "RGBA", "optional": true, "description": "The content box highlight fill color (default: transparent)." }, { "name": "paddingColor", "$ref": "RGBA", "optional": true, "description": "The padding highlight fill color (default: transparent)." }, { "name": "borderColor", "$ref": "RGBA", "optional": true, "description": "The border highlight fill color (default: transparent)." }, { "name": "marginColor", "$ref": "RGBA", "optional": true, "description": "The margin highlight fill color (default: transparent)." }, { "name": "eventTargetColor", "$ref": "RGBA", "optional": true, "hidden": true, "description": "The event target element highlight fill color (default: transparent)." }, { "name": "shapeColor", "$ref": "RGBA", "optional": true, "hidden": true, "description": "The shape outside fill color (default: transparent)." }, { "name": "shapeMarginColor", "$ref": "RGBA", "optional": true, "hidden": true, "description": "The shape margin fill color (default: transparent)." } ], "description": "Configuration data for the highlighting of page elements." } ], "commands": [ { "name": "enable", "description": "Enables DOM agent for the given page." }, { "name": "disable", "description": "Disables DOM agent for the given page." }, { "name": "getDocument", "returns": [ { "name": "root", "$ref": "Node", "description": "Resulting node." } ], "description": "Returns the root DOM node to the caller." }, { "name": "requestChildNodes", "parameters": [ { "name": "nodeId", "$ref": "NodeId", "description": "Id of the node to get children for." }, { "name": "depth", "type": "integer", "optional": true, "description": "The maximum depth at which children should be retrieved, defaults to 1. Use -1 for the entire subtree or provide an integer larger than 0.", "hidden": true } ], "description": "Requests that children of the node with given id are returned to the caller in form of setChildNodes events where not only immediate children are retrieved, but all children down to the specified depth." }, { "name": "querySelector", "parameters": [ { "name": "nodeId", "$ref": "NodeId", "description": "Id of the node to query upon." }, { "name": "selector", "type": "string", "description": "Selector string." } ], "returns": [ { "name": "nodeId", "$ref": "NodeId", "description": "Query selector result." } ], "description": "Executes querySelector on a given node." }, { "name": "querySelectorAll", "parameters": [ { "name": "nodeId", "$ref": "NodeId", "description": "Id of the node to query upon." }, { "name": "selector", "type": "string", "description": "Selector string." } ], "returns": [ { "name": "nodeIds", "type": "array", "items": { "$ref": "NodeId" }, "description": "Query selector result." } ], "description": "Executes querySelectorAll on a given node." }, { "name": "setNodeName", "parameters": [ { "name": "nodeId", "$ref": "NodeId", "description": "Id of the node to set name for." }, { "name": "name", "type": "string", "description": "New node's name." } ], "returns": [ { "name": "nodeId", "$ref": "NodeId", "description": "New node's id." } ], "description": "Sets node name for a node with given id." }, { "name": "setNodeValue", "parameters": [ { "name": "nodeId", "$ref": "NodeId", "description": "Id of the node to set value for." }, { "name": "value", "type": "string", "description": "New node's value." } ], "description": "Sets node value for a node with given id." }, { "name": "removeNode", "parameters": [ { "name": "nodeId", "$ref": "NodeId", "description": "Id of the node to remove." } ], "description": "Removes node with given id." }, { "name": "setAttributeValue", "parameters": [ { "name": "nodeId", "$ref": "NodeId", "description": "Id of the element to set attribute for." }, { "name": "name", "type": "string", "description": "Attribute name." }, { "name": "value", "type": "string", "description": "Attribute value." } ], "description": "Sets attribute for an element with given id." }, { "name": "setAttributesAsText", "parameters": [ { "name": "nodeId", "$ref": "NodeId", "description": "Id of the element to set attributes for." }, { "name": "text", "type": "string", "description": "Text with a number of attributes. Will parse this text using HTML parser." }, { "name": "name", "type": "string", "optional": true, "description": "Attribute name to replace with new attributes derived from text in case text parsed successfully." } ], "description": "Sets attributes on element with given id. This method is useful when user edits some existing attribute value and types in several attribute name/value pairs." }, { "name": "removeAttribute", "parameters": [ { "name": "nodeId", "$ref": "NodeId", "description": "Id of the element to remove attribute from." }, { "name": "name", "type": "string", "description": "Name of the attribute to remove." } ], "description": "Removes attribute with given name from an element with given id." }, { "name": "getOuterHTML", "parameters": [ { "name": "nodeId", "$ref": "NodeId", "description": "Id of the node to get markup for." } ], "returns": [ { "name": "outerHTML", "type": "string", "description": "Outer HTML markup." } ], "description": "Returns node's HTML markup." }, { "name": "setOuterHTML", "parameters": [ { "name": "nodeId", "$ref": "NodeId", "description": "Id of the node to set markup for." }, { "name": "outerHTML", "type": "string", "description": "Outer HTML markup to set." } ], "description": "Sets node HTML markup, returns new node id." }, { "name": "performSearch", "parameters": [ { "name": "query", "type": "string", "description": "Plain text or query selector or XPath search query." }, { "name": "includeUserAgentShadowDOM", "type": "boolean", "optional": true, "description": "True to search in user agent shadow DOM.", "hidden": true } ], "returns": [ { "name": "searchId", "type": "string", "description": "Unique search session identifier." }, { "name": "resultCount", "type": "integer", "description": "Number of search results." } ], "description": "Searches for a given string in the DOM tree. Use getSearchResults to access search results or cancelSearch to end this search session.", "hidden": true }, { "name": "getSearchResults", "parameters": [ { "name": "searchId", "type": "string", "description": "Unique search session identifier." }, { "name": "fromIndex", "type": "integer", "description": "Start index of the search result to be returned." }, { "name": "toIndex", "type": "integer", "description": "End index of the search result to be returned." } ], "returns": [ { "name": "nodeIds", "type": "array", "items": { "$ref": "NodeId" }, "description": "Ids of the search result nodes." } ], "description": "Returns search results from given fromIndex to given toIndex from the sarch with the given identifier.", "hidden": true }, { "name": "discardSearchResults", "parameters": [ { "name": "searchId", "type": "string", "description": "Unique search session identifier." } ], "description": "Discards search results from the session with the given id. getSearchResults should no longer be called for that search.", "hidden": true }, { "name": "requestNode", "parameters": [ { "name": "objectId", "$ref": "Runtime.RemoteObjectId", "description": "JavaScript object id to convert into node." } ], "returns": [ { "name": "nodeId", "$ref": "NodeId", "description": "Node id for given object." } ], "description": "Requests that the node is sent to the caller given the JavaScript node object reference. All nodes that form the path from the node to the root are also sent to the client as a series of setChildNodes notifications." }, { "name": "setInspectModeEnabled", "hidden": true, "parameters": [ { "name": "enabled", "type": "boolean", "description": "True to enable inspection mode, false to disable it." }, { "name": "inspectUAShadowDOM", "type": "boolean", "optional": true, "description": "True to enable inspection mode for user agent shadow DOM." }, { "name": "highlightConfig", "$ref": "HighlightConfig", "optional": true, "description": "A descriptor for the highlight appearance of hovered-over nodes. May be omitted if enabled == false." } ], "description": "Enters the 'inspect' mode. In this mode, elements that user is hovering over are highlighted. Backend then generates 'inspectNodeRequested' event upon element selection." }, { "name": "highlightRect", "parameters": [ { "name": "x", "type": "integer", "description": "X coordinate" }, { "name": "y", "type": "integer", "description": "Y coordinate" }, { "name": "width", "type": "integer", "description": "Rectangle width" }, { "name": "height", "type": "integer", "description": "Rectangle height" }, { "name": "color", "$ref": "RGBA", "optional": true, "description": "The highlight fill color (default: transparent)." }, { "name": "outlineColor", "$ref": "RGBA", "optional": true, "description": "The highlight outline color (default: transparent)." } ], "description": "Highlights given rectangle. Coordinates are absolute with respect to the main frame viewport." }, { "name": "highlightQuad", "parameters": [ { "name": "quad", "$ref": "Quad", "description": "Quad to highlight" }, { "name": "color", "$ref": "RGBA", "optional": true, "description": "The highlight fill color (default: transparent)." }, { "name": "outlineColor", "$ref": "RGBA", "optional": true, "description": "The highlight outline color (default: transparent)." } ], "description": "Highlights given quad. Coordinates are absolute with respect to the main frame viewport.", "hidden": true }, { "name": "highlightNode", "parameters": [ { "name": "highlightConfig", "$ref": "HighlightConfig", "description": "A descriptor for the highlight appearance." }, { "name": "nodeId", "$ref": "NodeId", "optional": true, "description": "Identifier of the node to highlight." }, { "name": "backendNodeId", "$ref": "BackendNodeId", "optional": true, "description": "Identifier of the backend node to highlight." }, { "name": "objectId", "$ref": "Runtime.RemoteObjectId", "optional": true, "description": "JavaScript object id of the node to be highlighted.", "hidden": true } ], "description": "Highlights DOM node with given id or with the given JavaScript object wrapper. Either nodeId or objectId must be specified." }, { "name": "hideHighlight", "description": "Hides DOM node highlight." }, { "name": "highlightFrame", "parameters": [ { "name": "frameId", "$ref": "Page.FrameId", "description": "Identifier of the frame to highlight." }, { "name": "contentColor", "$ref": "RGBA", "optional": true, "description": "The content box highlight fill color (default: transparent)." }, { "name": "contentOutlineColor", "$ref": "RGBA", "optional": true, "description": "The content box highlight outline color (default: transparent)." } ], "description": "Highlights owner element of the frame with given id.", "hidden": true }, { "name": "pushNodeByPathToFrontend", "parameters": [ { "name": "path", "type": "string", "description": "Path to node in the proprietary format." } ], "returns": [ { "name": "nodeId", "$ref": "NodeId", "description": "Id of the node for given path." } ], "description": "Requests that the node is sent to the caller given its path. // FIXME, use XPath", "hidden": true }, { "name": "pushNodesByBackendIdsToFrontend", "parameters": [ { "name": "backendNodeIds", "type": "array", "items": {"$ref": "BackendNodeId"}, "description": "The array of backend node ids." } ], "returns": [ { "name": "nodeIds", "type": "array", "items": {"$ref": "NodeId"}, "description": "The array of ids of pushed nodes that correspond to the backend ids specified in backendNodeIds." } ], "description": "Requests that a batch of nodes is sent to the caller given their backend node ids.", "hidden": true }, { "name": "setInspectedNode", "parameters": [ { "name": "nodeId", "$ref": "NodeId", "description": "DOM node id to be accessible by means of $x command line API." } ], "description": "Enables console to refer to the node with given id via $x (see Command Line API for more details $x functions).", "hidden": true }, { "name": "resolveNode", "parameters": [ { "name": "nodeId", "$ref": "NodeId", "description": "Id of the node to resolve." }, { "name": "objectGroup", "type": "string", "optional": true, "description": "Symbolic group name that can be used to release multiple objects." } ], "returns": [ { "name": "object", "$ref": "Runtime.RemoteObject", "description": "JavaScript object wrapper for given node." } ], "description": "Resolves JavaScript node object for given node id." }, { "name": "getAttributes", "parameters": [ { "name": "nodeId", "$ref": "NodeId", "description": "Id of the node to retrieve attibutes for." } ], "returns": [ { "name": "attributes", "type": "array", "items": { "type": "string" }, "description": "An interleaved array of node attribute names and values." } ], "description": "Returns attributes for the specified node." }, { "name": "copyTo", "parameters": [ { "name": "nodeId", "$ref": "NodeId", "description": "Id of the node to copy." }, { "name": "targetNodeId", "$ref": "NodeId", "description": "Id of the element to drop the copy into." }, { "name": "insertBeforeNodeId", "$ref": "NodeId", "optional": true, "description": "Drop the copy before this node (if absent, the copy becomes the last child of targetNodeId)." } ], "returns": [ { "name": "nodeId", "$ref": "NodeId", "description": "Id of the node clone." } ], "description": "Creates a deep copy of the specified node and places it into the target container before the given anchor.", "hidden": true }, { "name": "moveTo", "parameters": [ { "name": "nodeId", "$ref": "NodeId", "description": "Id of the node to move." }, { "name": "targetNodeId", "$ref": "NodeId", "description": "Id of the element to drop the moved node into." }, { "name": "insertBeforeNodeId", "$ref": "NodeId", "optional": true, "description": "Drop node before this one (if absent, the moved node becomes the last child of targetNodeId)." } ], "returns": [ { "name": "nodeId", "$ref": "NodeId", "description": "New id of the moved node." } ], "description": "Moves node into the new container, places it before the given anchor." }, { "name": "undo", "description": "Undoes the last performed action.", "hidden": true }, { "name": "redo", "description": "Re-does the last undone action.", "hidden": true }, { "name": "markUndoableState", "description": "Marks last undoable state.", "hidden": true }, { "name": "focus", "parameters": [ { "name": "nodeId", "$ref": "NodeId", "description": "Id of the node to focus." } ], "description": "Focuses the given element.", "hidden": true }, { "name": "setFileInputFiles", "parameters": [ { "name": "nodeId", "$ref": "NodeId", "description": "Id of the file input node to set files for." }, { "name": "files", "type": "array", "items": { "type": "string" }, "description": "Array of file paths to set." } ], "description": "Sets files for the given file input element.", "hidden": true, "handlers": ["browser", "renderer"] }, { "name": "getBoxModel", "parameters": [ { "name": "nodeId", "$ref": "NodeId", "description": "Id of the node to get box model for." } ], "returns": [ { "name": "model", "$ref": "BoxModel", "description": "Box model for the node." } ], "description": "Returns boxes for the currently selected nodes.", "hidden": true }, { "name": "getNodeForLocation", "parameters": [ { "name": "x", "type": "integer", "description": "X coordinate." }, { "name": "y", "type": "integer", "description": "Y coordinate." } ], "returns": [ { "name": "nodeId", "$ref": "NodeId", "description": "Id of the node at given coordinates." } ], "description": "Returns node id at given location.", "hidden": true }, { "name": "getRelayoutBoundary", "parameters": [ { "name": "nodeId", "$ref": "NodeId", "description": "Id of the node." } ], "returns": [ { "name": "nodeId", "$ref": "NodeId", "description": "Relayout boundary node id for the given node." } ], "description": "Returns the id of the nearest ancestor that is a relayout boundary.", "hidden": true }, { "name": "getHighlightObjectForTest", "parameters": [ { "name": "nodeId", "$ref": "NodeId", "description": "Id of the node to get highlight object for." } ], "returns": [ { "name": "highlight", "type": "object", "description": "Highlight data for the node." } ], "description": "For testing.", "hidden": true } ], "events": [ { "name": "documentUpdated", "description": "Fired when Document has been totally updated. Node ids are no longer valid." }, { "name": "inspectNodeRequested", "parameters": [ { "name": "backendNodeId", "$ref": "BackendNodeId", "description": "Id of the node to inspect." } ], "description": "Fired when the node should be inspected. This happens after call to setInspectModeEnabled.", "hidden" : true }, { "name": "setChildNodes", "parameters": [ { "name": "parentId", "$ref": "NodeId", "description": "Parent node id to populate with children." }, { "name": "nodes", "type": "array", "items": { "$ref": "Node" }, "description": "Child nodes array." } ], "description": "Fired when backend wants to provide client with the missing DOM structure. This happens upon most of the calls requesting node ids." }, { "name": "attributeModified", "parameters": [ { "name": "nodeId", "$ref": "NodeId", "description": "Id of the node that has changed." }, { "name": "name", "type": "string", "description": "Attribute name." }, { "name": "value", "type": "string", "description": "Attribute value." } ], "description": "Fired when Element's attribute is modified." }, { "name": "attributeRemoved", "parameters": [ { "name": "nodeId", "$ref": "NodeId", "description": "Id of the node that has changed." }, { "name": "name", "type": "string", "description": "A ttribute name." } ], "description": "Fired when Element's attribute is removed." }, { "name": "inlineStyleInvalidated", "parameters": [ { "name": "nodeIds", "type": "array", "items": { "$ref": "NodeId" }, "description": "Ids of the nodes for which the inline styles have been invalidated." } ], "description": "Fired when Element's inline style is modified via a CSS property modification.", "hidden": true }, { "name": "characterDataModified", "parameters": [ { "name": "nodeId", "$ref": "NodeId", "description": "Id of the node that has changed." }, { "name": "characterData", "type": "string", "description": "New text value." } ], "description": "Mirrors DOMCharacterDataModified event." }, { "name": "childNodeCountUpdated", "parameters": [ { "name": "nodeId", "$ref": "NodeId", "description": "Id of the node that has changed." }, { "name": "childNodeCount", "type": "integer", "description": "New node count." } ], "description": "Fired when Container's child node count has changed." }, { "name": "childNodeInserted", "parameters": [ { "name": "parentNodeId", "$ref": "NodeId", "description": "Id of the node that has changed." }, { "name": "previousNodeId", "$ref": "NodeId", "description": "If of the previous siblint." }, { "name": "node", "$ref": "Node", "description": "Inserted node data." } ], "description": "Mirrors DOMNodeInserted event." }, { "name": "childNodeRemoved", "parameters": [ { "name": "parentNodeId", "$ref": "NodeId", "description": "Parent id." }, { "name": "nodeId", "$ref": "NodeId", "description": "Id of the node that has been removed." } ], "description": "Mirrors DOMNodeRemoved event." }, { "name": "shadowRootPushed", "parameters": [ { "name": "hostId", "$ref": "NodeId", "description": "Host element id." }, { "name": "root", "$ref": "Node", "description": "Shadow root." } ], "description": "Called when shadow root is pushed into the element.", "hidden": true }, { "name": "shadowRootPopped", "parameters": [ { "name": "hostId", "$ref": "NodeId", "description": "Host element id." }, { "name": "rootId", "$ref": "NodeId", "description": "Shadow root id." } ], "description": "Called when shadow root is popped from the element.", "hidden": true }, { "name": "pseudoElementAdded", "parameters": [ { "name": "parentId", "$ref": "NodeId", "description": "Pseudo element's parent element id." }, { "name": "pseudoElement", "$ref": "Node", "description": "The added pseudo element." } ], "description": "Called when a pseudo element is added to an element.", "hidden": true }, { "name": "pseudoElementRemoved", "parameters": [ { "name": "parentId", "$ref": "NodeId", "description": "Pseudo element's parent element id." }, { "name": "pseudoElementId", "$ref": "NodeId", "description": "The removed pseudo element id." } ], "description": "Called when a pseudo element is removed from an element.", "hidden": true }, { "name": "distributedNodesUpdated", "parameters": [ { "name": "insertionPointId", "$ref": "NodeId", "description": "Insertion point where distrubuted nodes were updated." }, { "name": "distributedNodes", "type": "array", "items": { "$ref": "BackendNode" }, "description": "Distributed nodes for given insertion point." } ], "description": "Called when distrubution is changed.", "hidden": true } ] }, { "domain": "CSS", "hidden": true, "description": "This domain exposes CSS read/write operations. All CSS objects (stylesheets, rules, and styles) have an associated id used in subsequent operations on the related object. Each object type has a specific id structure, and those are not interchangeable between objects of different kinds. CSS objects can be loaded using the get*ForNode() calls (which accept a DOM node id). A client can also discover all the existing stylesheets with the getAllStyleSheets() method (or keeping track of the styleSheetAdded/styleSheetRemoved events) and subsequently load the required stylesheet contents using the getStyleSheet[Text]() methods.", "types": [ { "id": "StyleSheetId", "type": "string" }, { "id": "StyleSheetOrigin", "type": "string", "enum": ["injected", "user-agent", "inspector", "regular"], "description": "Stylesheet type: \"injected\" for stylesheets injected via extension, \"user-agent\" for user-agent stylesheets, \"inspector\" for stylesheets created by the inspector (i.e. those holding the \"via inspector\" rules), \"regular\" for regular stylesheets." }, { "id": "PseudoIdMatches", "type": "object", "properties": [ { "name": "pseudoId", "type": "integer", "description": "Pseudo style identifier (see enum PseudoId in ComputedStyleConstants.h)."}, { "name": "matches", "type": "array", "items": { "$ref": "RuleMatch" }, "description": "Matches of CSS rules applicable to the pseudo style."} ], "description": "CSS rule collection for a single pseudo style." }, { "id": "InheritedStyleEntry", "type": "object", "properties": [ { "name": "inlineStyle", "$ref": "CSSStyle", "optional": true, "description": "The ancestor node's inline style, if any, in the style inheritance chain." }, { "name": "matchedCSSRules", "type": "array", "items": { "$ref": "RuleMatch" }, "description": "Matches of CSS rules matching the ancestor node in the style inheritance chain." } ], "description": "Inherited CSS rule collection from ancestor node." }, { "id": "RuleMatch", "type": "object", "properties": [ { "name": "rule", "$ref": "CSSRule", "description": "CSS rule in the match." }, { "name": "matchingSelectors", "type": "array", "items": { "type": "integer" }, "description": "Matching selector indices in the rule's selectorList selectors (0-based)." } ], "description": "Match data for a CSS rule." }, { "id": "Selector", "type": "object", "properties": [ { "name": "value", "type": "string", "description": "Selector text." }, { "name": "range", "$ref": "SourceRange", "optional": true, "description": "Selector range in the underlying resource (if available)." } ], "description": "Data for a simple selector (these are delimited by commas in a selector list)." }, { "id": "SelectorList", "type": "object", "properties": [ { "name": "selectors", "type": "array", "items": { "$ref": "Selector" }, "description": "Selectors in the list." }, { "name": "text", "type": "string", "description": "Rule selector text." } ], "description": "Selector list data." }, { "id": "CSSStyleSheetHeader", "type": "object", "properties": [ { "name": "styleSheetId", "$ref": "StyleSheetId", "description": "The stylesheet identifier."}, { "name": "frameId", "$ref": "Page.FrameId", "description": "Owner frame identifier."}, { "name": "sourceURL", "type": "string", "description": "Stylesheet resource URL."}, { "name": "sourceMapURL", "type": "string", "optional": true, "description": "URL of source map associated with the stylesheet (if any)." }, { "name": "origin", "$ref": "StyleSheetOrigin", "description": "Stylesheet origin."}, { "name": "title", "type": "string", "description": "Stylesheet title."}, { "name": "ownerNode", "$ref": "DOM.BackendNodeId", "optional": true, "description": "The backend id for the owner node of the stylesheet." }, { "name": "disabled", "type": "boolean", "description": "Denotes whether the stylesheet is disabled."}, { "name": "hasSourceURL", "type": "boolean", "optional": true, "description": "Whether the sourceURL field value comes from the sourceURL comment." }, { "name": "isInline", "type": "boolean", "description": "Whether this stylesheet is created for STYLE tag by parser. This flag is not set for document.written STYLE tags." }, { "name": "startLine", "type": "number", "description": "Line offset of the stylesheet within the resource (zero based)." }, { "name": "startColumn", "type": "number", "description": "Column offset of the stylesheet within the resource (zero based)." } ], "description": "CSS stylesheet metainformation." }, { "id": "CSSRule", "type": "object", "properties": [ { "name": "styleSheetId", "$ref": "StyleSheetId", "optional": true, "description": "The css style sheet identifier (absent for user agent stylesheet and user-specified stylesheet rules) this rule came from." }, { "name": "selectorList", "$ref": "SelectorList", "description": "Rule selector data." }, { "name": "origin", "$ref": "StyleSheetOrigin", "description": "Parent stylesheet's origin."}, { "name": "style", "$ref": "CSSStyle", "description": "Associated style declaration." }, { "name": "media", "type": "array", "items": { "$ref": "CSSMedia" }, "optional": true, "description": "Media list array (for rules involving media queries). The array enumerates media queries starting with the innermost one, going outwards." } ], "description": "CSS rule representation." }, { "id": "SourceRange", "type": "object", "properties": [ { "name": "startLine", "type": "integer", "description": "Start line of range." }, { "name": "startColumn", "type": "integer", "description": "Start column of range (inclusive)." }, { "name": "endLine", "type": "integer", "description": "End line of range" }, { "name": "endColumn", "type": "integer", "description": "End column of range (exclusive)." } ], "description": "Text range within a resource. All numbers are zero-based." }, { "id": "ShorthandEntry", "type": "object", "properties": [ { "name": "name", "type": "string", "description": "Shorthand name." }, { "name": "value", "type": "string", "description": "Shorthand value." }, { "name": "important", "type": "boolean", "optional": true, "description": "Whether the property has \"!important\" annotation (implies false if absent)." } ] }, { "id": "CSSComputedStyleProperty", "type": "object", "properties": [ { "name": "name", "type": "string", "description": "Computed style property name." }, { "name": "value", "type": "string", "description": "Computed style property value." } ] }, { "id": "CSSStyle", "type": "object", "properties": [ { "name": "styleSheetId", "$ref": "StyleSheetId", "optional": true, "description": "The css style sheet identifier (absent for user agent stylesheet and user-specified stylesheet rules) this rule came from." }, { "name": "cssProperties", "type": "array", "items": { "$ref": "CSSProperty" }, "description": "CSS properties in the style." }, { "name": "shorthandEntries", "type": "array", "items": { "$ref": "ShorthandEntry" }, "description": "Computed values for all shorthands found in the style." }, { "name": "cssText", "type": "string", "optional": true, "description": "Style declaration text (if available)." }, { "name": "range", "$ref": "SourceRange", "optional": true, "description": "Style declaration range in the enclosing stylesheet (if available)." } ], "description": "CSS style representation." }, { "id": "CSSProperty", "type": "object", "properties": [ { "name": "name", "type": "string", "description": "The property name." }, { "name": "value", "type": "string", "description": "The property value." }, { "name": "important", "type": "boolean", "optional": true, "description": "Whether the property has \"!important\" annotation (implies false if absent)." }, { "name": "implicit", "type": "boolean", "optional": true, "description": "Whether the property is implicit (implies false if absent)." }, { "name": "text", "type": "string", "optional": true, "description": "The full property text as specified in the style." }, { "name": "parsedOk", "type": "boolean", "optional": true, "description": "Whether the property is understood by the browser (implies true if absent)." }, { "name": "disabled", "type": "boolean", "optional": true, "description": "Whether the property is disabled by the user (present for source-based properties only)." }, { "name": "range", "$ref": "SourceRange", "optional": true, "description": "The entire property range in the enclosing style declaration (if available)." } ], "description": "CSS property declaration data." }, { "id": "CSSMedia", "type": "object", "properties": [ { "name": "text", "type": "string", "description": "Media query text." }, { "name": "source", "type": "string", "enum": ["mediaRule", "importRule", "linkedSheet", "inlineSheet"], "description": "Source of the media query: \"mediaRule\" if specified by a @media rule, \"importRule\" if specified by an @import rule, \"linkedSheet\" if specified by a \"media\" attribute in a linked stylesheet's LINK tag, \"inlineSheet\" if specified by a \"media\" attribute in an inline stylesheet's STYLE tag." }, { "name": "sourceURL", "type": "string", "optional": true, "description": "URL of the document containing the media query description." }, { "name": "range", "$ref": "SourceRange", "optional": true, "description": "The associated rule (@media or @import) header range in the enclosing stylesheet (if available)." }, { "name": "parentStyleSheetId", "$ref": "StyleSheetId", "optional": true, "description": "Identifier of the stylesheet containing this object (if exists)." }, { "name": "mediaList", "type": "array", "items": { "$ref": "MediaQuery" }, "optional": true, "hidden": true, "description": "Array of media queries." } ], "description": "CSS media rule descriptor." }, { "id": "MediaQuery", "type": "object", "properties": [ { "name": "expressions", "type": "array", "items": { "$ref": "MediaQueryExpression" }, "description": "Array of media query expressions." }, { "name": "active", "type": "boolean", "description": "Whether the media query condition is satisfied." } ], "description": "Media query descriptor.", "hidden": true }, { "id": "MediaQueryExpression", "type": "object", "properties": [ { "name": "value", "type": "number", "description": "Media query expression value."}, { "name": "unit", "type": "string", "description": "Media query expression units."}, { "name": "feature", "type": "string", "description": "Media query expression feature."}, { "name": "valueRange", "$ref": "SourceRange", "optional": true, "description": "The associated range of the value text in the enclosing stylesheet (if available)." }, { "name": "computedLength", "type": "number", "optional": true, "description": "Computed length of media query expression (if applicable)."} ], "description": "Media query expression descriptor.", "hidden": true }, { "id": "PlatformFontUsage", "type": "object", "properties": [ { "name": "familyName", "type": "string", "description": "Font's family name reported by platform."}, { "name": "glyphCount", "type": "number", "description": "Amount of glyphs that were rendered with this font."} ], "description": "Information about amount of glyphs that were rendered with given font.", "hidden": true } ], "commands": [ { "name": "enable", "async": true, "description": "Enables the CSS agent for the given page. Clients should not assume that the CSS agent has been enabled until the result of this command is received." }, { "name": "disable", "description": "Disables the CSS agent for the given page." }, { "name": "getMatchedStylesForNode", "parameters": [ { "name": "nodeId", "$ref": "DOM.NodeId" }, { "name": "excludePseudo", "type": "boolean", "optional": true, "description": "Whether to exclude pseudo styles (default: false)." }, { "name": "excludeInherited", "type": "boolean", "optional": true, "description": "Whether to exclude inherited styles (default: false)." } ], "returns": [ { "name": "matchedCSSRules", "type": "array", "items": { "$ref": "RuleMatch" }, "optional": true, "description": "CSS rules matching this node, from all applicable stylesheets." }, { "name": "pseudoElements", "type": "array", "items": { "$ref": "PseudoIdMatches" }, "optional": true, "description": "Pseudo style matches for this node." }, { "name": "inherited", "type": "array", "items": { "$ref": "InheritedStyleEntry" }, "optional": true, "description": "A chain of inherited styles (from the immediate node parent up to the DOM tree root)." } ], "description": "Returns requested styles for a DOM node identified by nodeId." }, { "name": "getInlineStylesForNode", "parameters": [ { "name": "nodeId", "$ref": "DOM.NodeId" } ], "returns": [ { "name": "inlineStyle", "$ref": "CSSStyle", "optional": true, "description": "Inline style for the specified DOM node." }, { "name": "attributesStyle", "$ref": "CSSStyle", "optional": true, "description": "Attribute-defined element style (e.g. resulting from \"width=20 height=100%\")."} ], "description": "Returns the styles defined inline (explicitly in the \"style\" attribute and implicitly, using DOM attributes) for a DOM node identified by nodeId." }, { "name": "getComputedStyleForNode", "parameters": [ { "name": "nodeId", "$ref": "DOM.NodeId" } ], "returns": [ { "name": "computedStyle", "type": "array", "items": { "$ref": "CSSComputedStyleProperty" }, "description": "Computed style for the specified DOM node." } ], "description": "Returns the computed style for a DOM node identified by nodeId." }, { "name": "getPlatformFontsForNode", "parameters": [ { "name": "nodeId", "$ref": "DOM.NodeId" } ], "returns": [ { "name": "fonts", "type": "array", "items": { "$ref": "PlatformFontUsage" }, "description": "Usage statistics for every employed platform font." } ], "description": "Requests information about platform fonts which we used to render child TextNodes in the given node.", "hidden": true }, { "name": "getStyleSheetText", "parameters": [ { "name": "styleSheetId", "$ref": "StyleSheetId" } ], "returns": [ { "name": "text", "type": "string", "description": "The stylesheet text." } ], "description": "Returns the current textual content and the URL for a stylesheet." }, { "name": "setStyleSheetText", "parameters": [ { "name": "styleSheetId", "$ref": "StyleSheetId" }, { "name": "text", "type": "string" } ], "description": "Sets the new stylesheet text." }, { "name": "setRuleSelector", "parameters": [ { "name": "styleSheetId", "$ref": "StyleSheetId" }, { "name": "range", "$ref": "SourceRange" }, { "name": "selector", "type": "string" } ], "returns": [ { "name": "rule", "$ref": "CSSRule", "description": "The resulting rule after the selector modification." } ], "description": "Modifies the rule selector." }, { "name": "setStyleText", "parameters": [ { "name": "styleSheetId", "$ref": "StyleSheetId" }, { "name": "range", "$ref": "SourceRange" }, { "name": "text", "type": "string" } ], "returns": [ { "name": "style", "$ref": "CSSStyle", "description": "The resulting style after the selector modification." } ], "description": "Modifies the style text." }, { "name": "setMediaText", "parameters": [ { "name": "styleSheetId", "$ref": "StyleSheetId" }, { "name": "range", "$ref": "SourceRange" }, { "name": "text", "type": "string" } ], "returns": [ { "name": "media", "$ref": "CSSMedia", "description": "The resulting CSS media rule after modification." } ], "description": "Modifies the rule selector." }, { "name": "createStyleSheet", "parameters": [ { "name": "frameId", "$ref": "Page.FrameId", "description": "Identifier of the frame where \"via-inspector\" stylesheet should be created."} ], "returns": [ { "name": "styleSheetId", "$ref": "StyleSheetId", "description": "Identifier of the created \"via-inspector\" stylesheet." } ], "description": "Creates a new special \"via-inspector\" stylesheet in the frame with given frameId." }, { "name": "addRule", "parameters": [ { "name": "styleSheetId", "$ref": "StyleSheetId", "description": "The css style sheet identifier where a new rule should be inserted." }, { "name": "ruleText", "type": "string", "description": "The text of a new rule." }, { "name": "location", "$ref": "SourceRange", "description": "Text position of a new rule in the target style sheet." } ], "returns": [ { "name": "rule", "$ref": "CSSRule", "description": "The newly created rule." } ], "description": "Inserts a new rule with the given ruleText in a stylesheet with given styleSheetId, at the position specified by location." }, { "name": "forcePseudoState", "parameters": [ { "name": "nodeId", "$ref": "DOM.NodeId", "description": "The element id for which to force the pseudo state." }, { "name": "forcedPseudoClasses", "type": "array", "items": { "type": "string", "enum": ["active", "focus", "hover", "visited"] }, "description": "Element pseudo classes to force when computing the element's style." } ], "description": "Ensures that the given node will have specified pseudo-classes whenever its style is computed by the browser." }, { "name": "getMediaQueries", "returns": [ { "name": "medias", "type": "array", "items": { "$ref": "CSSMedia" } } ], "description": "Returns all media queries parsed by the rendering engine.", "hidden": true }, { "name": "setEffectivePropertyValueForNode", "parameters": [ { "name": "nodeId", "$ref": "DOM.NodeId", "description": "The element id for which to set property." }, { "name": "propertyName", "type": "string"}, { "name": "value", "type": "string"} ], "description": "Find a rule with the given active property for the given node and set the new value for this property", "hidden": true } ], "events": [ { "name": "mediaQueryResultChanged", "description": "Fires whenever a MediaQuery result changes (for example, after a browser window has been resized.) The current implementation considers only viewport-dependent media features." }, { "name": "styleSheetChanged", "parameters": [ { "name": "styleSheetId", "$ref": "StyleSheetId" } ], "description": "Fired whenever a stylesheet is changed as a result of the client operation." }, { "name": "styleSheetAdded", "parameters": [ { "name": "header", "$ref": "CSSStyleSheetHeader", "description": "Added stylesheet metainfo." } ], "description": "Fired whenever an active document stylesheet is added." }, { "name": "styleSheetRemoved", "parameters": [ { "name": "styleSheetId", "$ref": "StyleSheetId", "description": "Identifier of the removed stylesheet." } ], "description": "Fired whenever an active document stylesheet is removed." } ] }, { "domain": "Timeline", "description": "Timeline domain is deprecated. Please use Tracing instead.", "types": [ { "id": "TimelineEvent", "type": "object", "properties": [ { "name": "type", "type": "string", "description": "Event type." }, { "name": "data", "type": "object", "description": "Event data." }, { "name": "startTime", "type": "number", "description": "Start time." }, { "name": "endTime", "type": "number", "optional": true, "description": "End time." }, { "name": "children", "type": "array", "optional": true, "items": { "$ref": "TimelineEvent" }, "description": "Nested records." }, { "name": "thread", "type": "string", "optional": true, "hidden": true, "description": "If present, identifies the thread that produced the event." }, { "name": "stackTrace", "$ref": "Console.StackTrace", "optional": true, "hidden": true, "description": "Stack trace." }, { "name": "frameId", "type": "string", "optional": true, "hidden": true, "description": "Unique identifier of the frame within the page that the event relates to." } ], "description": "Timeline record contains information about the recorded activity." } ], "commands": [ { "name": "enable", "description": "Deprecated." }, { "name": "disable", "description": "Deprecated." }, { "name": "start", "parameters": [ { "name": "maxCallStackDepth", "optional": true, "type": "integer", "description": "Samples JavaScript stack traces up to maxCallStackDepth, defaults to 5." }, { "name": "bufferEvents", "optional": true, "type": "boolean", "hidden": true, "description": "Whether instrumentation events should be buffered and returned upon stop call." }, { "name": "liveEvents", "optional": true, "type": "string", "hidden": true, "description": "Coma separated event types to issue although bufferEvents is set."}, { "name": "includeCounters", "optional": true, "type": "boolean", "hidden": true, "description": "Whether counters data should be included into timeline events." }, { "name": "includeGPUEvents", "optional": true, "type": "boolean", "hidden": true, "description": "Whether events from GPU process should be collected." } ], "description": "Deprecated." }, { "name": "stop", "description": "Deprecated." } ], "events": [ { "name": "eventRecorded", "parameters": [ { "name": "record", "$ref": "TimelineEvent", "description": "Timeline event record data." } ], "description": "Deprecated." } ] }, { "domain": "Debugger", "description": "Debugger domain exposes JavaScript debugging capabilities. It allows setting and removing breakpoints, stepping through execution, exploring stack traces, etc.", "types": [ { "id": "BreakpointId", "type": "string", "description": "Breakpoint identifier." }, { "id": "ScriptId", "type": "string", "description": "Unique script identifier." }, { "id": "CallFrameId", "type": "string", "description": "Call frame identifier." }, { "id": "Location", "type": "object", "properties": [ { "name": "scriptId", "$ref": "ScriptId", "description": "Script identifier as reported in the Debugger.scriptParsed." }, { "name": "lineNumber", "type": "integer", "description": "Line number in the script (0-based)." }, { "name": "columnNumber", "type": "integer", "optional": true, "description": "Column number in the script (0-based)." } ], "description": "Location in the source code." }, { "id": "FunctionDetails", "hidden": true, "type": "object", "properties": [ { "name": "location", "$ref": "Location", "optional": true, "description": "Location of the function, none for native functions." }, { "name": "functionName", "type": "string", "description": "Name of the function." }, { "name": "isGenerator", "type": "boolean", "description": "Whether this is a generator function." }, { "name": "scopeChain", "type": "array", "optional": true, "items": { "$ref": "Scope" }, "description": "Scope chain for this closure." } ], "description": "Information about the function." }, { "id": "GeneratorObjectDetails", "hidden": true, "type": "object", "properties": [ { "name": "function", "$ref": "Runtime.RemoteObject", "description": "Generator function." }, { "name": "functionName", "type": "string", "description": "Name of the generator function." }, { "name": "status", "type": "string", "enum": ["running", "suspended", "closed"], "description": "Current generator object status." }, { "name": "location", "$ref": "Location", "optional": true, "description": "If suspended, location where generator function was suspended (e.g. location of the last 'yield'). Otherwise, location of the generator function." } ], "description": "Information about the generator object." }, { "id": "CollectionEntry", "hidden": true, "type": "object", "properties": [ { "name": "key", "$ref": "Runtime.RemoteObject", "optional": true, "description": "Entry key of a map-like collection, otherwise not provided." }, { "name": "value", "$ref": "Runtime.RemoteObject", "description": "Entry value." } ], "description": "Collection entry." }, { "id": "CallFrame", "type": "object", "properties": [ { "name": "callFrameId", "$ref": "CallFrameId", "description": "Call frame identifier. This identifier is only valid while the virtual machine is paused." }, { "name": "functionName", "type": "string", "description": "Name of the JavaScript function called on this call frame." }, { "name": "functionLocation", "$ref": "Location", "optional": true, "hidden": true, "description": "Location in the source code." }, { "name": "location", "$ref": "Location", "description": "Location in the source code." }, { "name": "scopeChain", "type": "array", "items": { "$ref": "Scope" }, "description": "Scope chain for this call frame." }, { "name": "this", "$ref": "Runtime.RemoteObject", "description": "this object for this call frame." }, { "name": "returnValue", "$ref": "Runtime.RemoteObject", "optional": true, "hidden": true, "description": "The value being returned, if the function is at return point." } ], "description": "JavaScript call frame. Array of call frames form the call stack." }, { "id": "StackTrace", "type": "object", "properties": [ { "name": "callFrames", "type": "array", "items": { "$ref": "CallFrame" }, "description": "Call frames of the stack trace." }, { "name": "description", "type": "string", "optional": true, "description": "String label of this stack trace. For async traces this may be a name of the function that initiated the async call." }, { "name": "asyncStackTrace", "$ref": "StackTrace", "optional": true, "description": "Async stack trace, if any." } ], "description": "JavaScript call stack, including async stack traces.", "hidden": true }, { "id": "Scope", "type": "object", "properties": [ { "name": "type", "type": "string", "enum": ["global", "local", "with", "closure", "catch", "block", "script"], "description": "Scope type." }, { "name": "object", "$ref": "Runtime.RemoteObject", "description": "Object representing the scope. For global and with scopes it represents the actual object; for the rest of the scopes, it is artificial transient object enumerating scope variables as its properties." } ], "description": "Scope description." }, { "id": "ExceptionDetails", "type": "object", "description": "Detailed information on exception (or error) that was thrown during script compilation or execution.", "properties": [ { "name": "text", "type": "string", "description": "Exception text." }, { "name": "url", "type": "string", "optional": true, "description": "URL of the message origin." }, { "name": "scriptId", "type": "string", "optional": true, "description": "Script ID of the message origin." }, { "name": "line", "type": "integer", "optional": true, "description": "Line number in the resource that generated this message." }, { "name": "column", "type": "integer", "optional": true, "description": "Column number in the resource that generated this message." }, { "name": "stackTrace", "$ref": "Console.StackTrace", "optional": true, "description": "JavaScript stack trace for assertions and error messages." } ] }, { "id": "SetScriptSourceError", "type": "object", "properties": [ { "name": "compileError", "optional": true, "type": "object", "properties": [ { "name": "message", "type": "string", "description": "Compiler error message" }, { "name": "lineNumber", "type": "integer", "description": "Compile error line number (1-based)" }, { "name": "columnNumber", "type": "integer", "description": "Compile error column number (1-based)" } ] } ], "description": "Error data for setScriptSource command. compileError is a case type for uncompilable script source error.", "hidden": true }, { "id": "PromiseDetails", "type": "object", "description": "Information about the promise. All fields but id are optional and if present they reflect the new state of the property on the promise with given id.", "properties": [ { "name": "id", "type": "integer", "description": "Unique id of the promise." }, { "name": "status", "type": "string", "optional": true, "enum": ["pending", "resolved", "rejected"], "description": "Status of the promise." }, { "name": "parentId", "type": "integer", "optional": true, "description": "Id of the parent promise." }, { "name": "callFrame", "$ref": "Console.CallFrame", "optional": true, "description": "Top call frame on promise creation."}, { "name": "creationTime", "type": "number", "optional": true, "description": "Creation time of the promise." }, { "name": "settlementTime", "type": "number", "optional": true, "description": "Settlement time of the promise." }, { "name": "creationStack", "$ref": "Console.StackTrace", "optional": true, "description": "JavaScript stack trace on promise creation." }, { "name": "asyncCreationStack", "$ref": "Console.AsyncStackTrace", "optional": true, "description": "JavaScript asynchronous stack trace on promise creation, if available." }, { "name": "settlementStack", "$ref": "Console.StackTrace", "optional": true, "description": "JavaScript stack trace on promise settlement." }, { "name": "asyncSettlementStack", "$ref": "Console.AsyncStackTrace", "optional": true, "description": "JavaScript asynchronous stack trace on promise settlement, if available." } ], "hidden": true }, { "id": "AsyncOperation", "type": "object", "description": "Information about the async operation.", "properties": [ { "name": "id", "type": "integer", "description": "Unique id of the async operation." }, { "name": "description", "type": "string", "description": "String description of the async operation." }, { "name": "stackTrace", "$ref": "Console.StackTrace", "optional": true, "description": "Stack trace where async operation was scheduled." }, { "name": "asyncStackTrace", "$ref": "Console.AsyncStackTrace", "optional": true, "description": "Asynchronous stack trace where async operation was scheduled, if available." } ], "hidden": true }, { "id": "SearchMatch", "type": "object", "description": "Search match for resource.", "properties": [ { "name": "lineNumber", "type": "number", "description": "Line number in resource content." }, { "name": "lineContent", "type": "string", "description": "Line with match content." } ], "hidden": true } ], "commands": [ { "name": "enable", "description": "Enables debugger for the given page. Clients should not assume that the debugging has been enabled until the result for this command is received." }, { "name": "disable", "description": "Disables debugger for given page." }, { "name": "setBreakpointsActive", "parameters": [ { "name": "active", "type": "boolean", "description": "New value for breakpoints active state." } ], "description": "Activates / deactivates all breakpoints on the page." }, { "name": "setSkipAllPauses", "hidden": true, "parameters": [ { "name": "skipped", "type": "boolean", "description": "New value for skip pauses state." } ], "description": "Makes page not interrupt on any pauses (breakpoint, exception, dom exception etc)." }, { "name": "setBreakpointByUrl", "parameters": [ { "name": "lineNumber", "type": "integer", "description": "Line number to set breakpoint at." }, { "name": "url", "type": "string", "optional": true, "description": "URL of the resources to set breakpoint on." }, { "name": "urlRegex", "type": "string", "optional": true, "description": "Regex pattern for the URLs of the resources to set breakpoints on. Either url or urlRegex must be specified." }, { "name": "columnNumber", "type": "integer", "optional": true, "description": "Offset in the line to set breakpoint at." }, { "name": "condition", "type": "string", "optional": true, "description": "Expression to use as a breakpoint condition. When specified, debugger will only stop on the breakpoint if this expression evaluates to true." } ], "returns": [ { "name": "breakpointId", "$ref": "BreakpointId", "description": "Id of the created breakpoint for further reference." }, { "name": "locations", "type": "array", "items": { "$ref": "Location" }, "description": "List of the locations this breakpoint resolved into upon addition." } ], "description": "Sets JavaScript breakpoint at given location specified either by URL or URL regex. Once this command is issued, all existing parsed scripts will have breakpoints resolved and returned in locations property. Further matching script parsing will result in subsequent breakpointResolved events issued. This logical breakpoint will survive page reloads." }, { "name": "setBreakpoint", "parameters": [ { "name": "location", "$ref": "Location", "description": "Location to set breakpoint in." }, { "name": "condition", "type": "string", "optional": true, "description": "Expression to use as a breakpoint condition. When specified, debugger will only stop on the breakpoint if this expression evaluates to true." } ], "returns": [ { "name": "breakpointId", "$ref": "BreakpointId", "description": "Id of the created breakpoint for further reference." }, { "name": "actualLocation", "$ref": "Location", "description": "Location this breakpoint resolved into." } ], "description": "Sets JavaScript breakpoint at a given location." }, { "name": "removeBreakpoint", "parameters": [ { "name": "breakpointId", "$ref": "BreakpointId" } ], "description": "Removes JavaScript breakpoint." }, { "name": "continueToLocation", "parameters": [ { "name": "location", "$ref": "Location", "description": "Location to continue to." }, { "name": "interstatementLocation", "type": "boolean", "optional": true, "hidden": true, "description": "Allows breakpoints at the intemediate positions inside statements." } ], "description": "Continues execution until specific location is reached." }, { "name": "stepOver", "description": "Steps over the statement." }, { "name": "stepInto", "description": "Steps into the function call." }, { "name": "stepOut", "description": "Steps out of the function call." }, { "name": "pause", "description": "Stops on the next JavaScript statement." }, { "name": "resume", "description": "Resumes JavaScript execution." }, { "name": "stepIntoAsync", "description": "Steps into the first async operation handler that was scheduled by or after the current statement.", "hidden": true }, { "name": "searchInContent", "parameters": [ { "name": "scriptId", "$ref": "ScriptId", "description": "Id of the script to search in." }, { "name": "query", "type": "string", "description": "String to search for." }, { "name": "caseSensitive", "type": "boolean", "optional": true, "description": "If true, search is case sensitive." }, { "name": "isRegex", "type": "boolean", "optional": true, "description": "If true, treats string parameter as regex." } ], "returns": [ { "name": "result", "type": "array", "items": { "$ref": "SearchMatch" }, "description": "List of search matches." } ], "description": "Searches for given string in script content." }, { "name": "canSetScriptSource", "returns": [ { "name": "result", "type": "boolean", "description": "True if setScriptSource is supported." } ], "description": "Always returns true." }, { "name": "setScriptSource", "parameters": [ { "name": "scriptId", "$ref": "ScriptId", "description": "Id of the script to edit." }, { "name": "scriptSource", "type": "string", "description": "New content of the script." }, { "name": "preview", "type": "boolean", "optional": true, "description": " If true the change will not actually be applied. Preview mode may be used to get result description without actually modifying the code.", "hidden": true } ], "returns": [ { "name": "callFrames", "type": "array", "optional": true, "items": { "$ref": "CallFrame" }, "description": "New stack trace in case editing has happened while VM was stopped." }, { "name": "result", "type": "object", "optional": true, "description": "VM-specific description of the changes applied.", "hidden": true }, { "name": "asyncStackTrace", "$ref": "StackTrace", "optional": true, "description": "Async stack trace, if any.", "hidden": true } ], "error": { "$ref": "SetScriptSourceError" }, "description": "Edits JavaScript source live." }, { "name": "restartFrame", "parameters": [ { "name": "callFrameId", "$ref": "CallFrameId", "description": "Call frame identifier to evaluate on." } ], "returns": [ { "name": "callFrames", "type": "array", "items": { "$ref": "CallFrame" }, "description": "New stack trace." }, { "name": "result", "type": "object", "description": "VM-specific description." }, { "name": "asyncStackTrace", "$ref": "StackTrace", "optional": true, "description": "Async stack trace, if any." } ], "hidden": true, "description": "Restarts particular call frame from the beginning." }, { "name": "getScriptSource", "parameters": [ { "name": "scriptId", "$ref": "ScriptId", "description": "Id of the script to get source for." } ], "returns": [ { "name": "scriptSource", "type": "string", "description": "Script source." } ], "description": "Returns source for the script with given id." }, { "name": "getFunctionDetails", "hidden": true, "parameters": [ { "name": "functionId", "$ref": "Runtime.RemoteObjectId", "description": "Id of the function to get details for." } ], "returns": [ { "name": "details", "$ref": "FunctionDetails", "description": "Information about the function." } ], "description": "Returns detailed information on given function." }, { "name": "getGeneratorObjectDetails", "hidden": true, "parameters": [ { "name": "objectId", "$ref": "Runtime.RemoteObjectId", "description": "Id of the generator object to get details for." } ], "returns": [ { "name": "details", "$ref": "GeneratorObjectDetails", "description": "Information about the generator object." } ], "description": "Returns detailed information on given generator object." }, { "name": "getCollectionEntries", "hidden": true, "parameters": [ { "name": "objectId", "$ref": "Runtime.RemoteObjectId", "description": "Id of the collection to get entries for." } ], "returns": [ { "name": "entries", "type": "array", "items": { "$ref": "CollectionEntry" }, "description": "Array of collection entries." } ], "description": "Returns entries of given collection." }, { "name": "setPauseOnExceptions", "parameters": [ { "name": "state", "type": "string", "enum": ["none", "uncaught", "all"], "description": "Pause on exceptions mode." } ], "description": "Defines pause on exceptions state. Can be set to stop on all exceptions, uncaught exceptions or no exceptions. Initial pause on exceptions state is none." }, { "name": "evaluateOnCallFrame", "parameters": [ { "name": "callFrameId", "$ref": "CallFrameId", "description": "Call frame identifier to evaluate on." }, { "name": "expression", "type": "string", "description": "Expression to evaluate." }, { "name": "objectGroup", "type": "string", "optional": true, "description": "String object group name to put result into (allows rapid releasing resulting object handles using releaseObjectGroup)." }, { "name": "includeCommandLineAPI", "type": "boolean", "optional": true, "description": "Specifies whether command line API should be available to the evaluated expression, defaults to false.", "hidden": true }, { "name": "doNotPauseOnExceptionsAndMuteConsole", "type": "boolean", "optional": true, "description": "Specifies whether evaluation should stop on exceptions and mute console. Overrides setPauseOnException state.", "hidden": true }, { "name": "returnByValue", "type": "boolean", "optional": true, "description": "Whether the result is expected to be a JSON object that should be sent by value." }, { "name": "generatePreview", "type": "boolean", "optional": true, "hidden": true, "description": "Whether preview should be generated for the result." } ], "returns": [ { "name": "result", "$ref": "Runtime.RemoteObject", "description": "Object wrapper for the evaluation result." }, { "name": "wasThrown", "type": "boolean", "optional": true, "description": "True if the result was thrown during the evaluation." }, { "name": "exceptionDetails", "$ref": "ExceptionDetails", "optional": true, "hidden": true, "description": "Exception details."} ], "description": "Evaluates expression on a given call frame." }, { "name": "compileScript", "hidden": true, "parameters": [ { "name": "expression", "type": "string", "description": "Expression to compile." }, { "name": "sourceURL", "type": "string", "description": "Source url to be set for the script." }, { "name": "persistScript", "type": "boolean", "description": "Specifies whether the compiled script should be persisted." }, { "name": "executionContextId", "$ref": "Runtime.ExecutionContextId", "optional": true, "description": "Specifies in which isolated context to perform script run. Each content script lives in an isolated context and this parameter may be used to specify one of those contexts. If the parameter is omitted or 0 the evaluation will be performed in the context of the inspected page." } ], "returns": [ { "name": "scriptId", "$ref": "ScriptId", "optional": true, "description": "Id of the script." }, { "name": "exceptionDetails", "$ref": "ExceptionDetails", "optional": true, "description": "Exception details."} ], "description": "Compiles expression." }, { "name": "runScript", "hidden": true, "parameters": [ { "name": "scriptId", "$ref": "ScriptId", "description": "Id of the script to run." }, { "name": "executionContextId", "$ref": "Runtime.ExecutionContextId", "optional": true, "description": "Specifies in which isolated context to perform script run. Each content script lives in an isolated context and this parameter may be used to specify one of those contexts. If the parameter is omitted or 0 the evaluation will be performed in the context of the inspected page." }, { "name": "objectGroup", "type": "string", "optional": true, "description": "Symbolic group name that can be used to release multiple objects." }, { "name": "doNotPauseOnExceptionsAndMuteConsole", "type": "boolean", "optional": true, "description": "Specifies whether script run should stop on exceptions and mute console. Overrides setPauseOnException state." } ], "returns": [ { "name": "result", "$ref": "Runtime.RemoteObject", "description": "Run result." }, { "name": "exceptionDetails", "$ref": "ExceptionDetails", "optional": true, "description": "Exception details."} ], "description": "Runs script with given id in a given context." }, { "name": "setVariableValue", "parameters": [ { "name": "scopeNumber", "type": "integer", "description": "0-based number of scope as was listed in scope chain. Only 'local', 'closure' and 'catch' scope types are allowed. Other scopes could be manipulated manually." }, { "name": "variableName", "type": "string", "description": "Variable name." }, { "name": "newValue", "$ref": "Runtime.CallArgument", "description": "New variable value." }, { "name": "callFrameId", "$ref": "CallFrameId", "optional": true, "description": "Id of callframe that holds variable." }, { "name": "functionObjectId", "$ref": "Runtime.RemoteObjectId", "optional": true, "description": "Object id of closure (function) that holds variable." } ], "hidden": true, "description": "Changes value of variable in a callframe or a closure. Either callframe or function must be specified. Object-based scopes are not supported and must be mutated manually." }, { "name": "getStepInPositions", "parameters": [ { "name": "callFrameId", "$ref": "CallFrameId", "description": "Id of a call frame where the current statement should be analized" } ], "returns": [ { "name": "stepInPositions", "type": "array", "items": { "$ref": "Location" }, "optional": true, "description": "experimental" } ], "hidden": true, "description": "Lists all positions where step-in is possible for a current statement in a specified call frame" }, { "name": "getBacktrace", "returns": [ { "name": "callFrames", "type": "array", "items": { "$ref": "CallFrame" }, "description": "Call stack the virtual machine stopped on." }, { "name": "asyncStackTrace", "$ref": "StackTrace", "optional": true, "description": "Async stack trace, if any." } ], "hidden": true, "description": "Returns call stack including variables changed since VM was paused. VM must be paused." }, { "name": "skipStackFrames", "parameters": [ { "name": "script", "type": "string", "optional": true, "description": "Regular expression defining the scripts to ignore while stepping." }, { "name": "skipContentScripts", "type": "boolean", "optional": true, "description": "True, if all content scripts should be ignored." } ], "hidden": true, "description": "Makes backend skip steps in the sources with names matching given pattern. VM will try leave blacklisted scripts by performing 'step in' several times, finally resorting to 'step out' if unsuccessful." }, { "name": "setAsyncCallStackDepth", "parameters": [ { "name": "maxDepth", "type": "integer", "description": "Maximum depth of async call stacks. Setting to 0 will effectively disable collecting async call stacks (default)." } ], "hidden": true, "description": "Enables or disables async call stacks tracking." }, { "name": "enablePromiseTracker", "parameters": [ { "name": "captureStacks", "type": "boolean", "optional": true, "description": "Whether to capture stack traces for promise creation and settlement events (default: false)." } ], "hidden": true, "description": "Enables promise tracking, information about Promises created or updated will now be stored on the backend." }, { "name": "disablePromiseTracker", "hidden": true, "description": "Disables promise tracking." }, { "name": "getPromiseById", "parameters": [ { "name": "promiseId", "type": "integer" }, { "name": "objectGroup", "type": "string", "optional": true, "description": "Symbolic group name that can be used to release multiple objects." } ], "returns": [ { "name": "promise", "$ref": "Runtime.RemoteObject", "description": "Object wrapper for Promise with specified ID, if any." } ], "hidden": true, "description": "Returns Promise with specified ID." }, { "name": "flushAsyncOperationEvents", "hidden": true, "description": "Fires pending asyncOperationStarted events (if any), as if a debugger stepping session has just been started." }, { "name": "setAsyncOperationBreakpoint", "parameters": [ { "name": "operationId", "type": "integer", "description": "ID of the async operation to set breakpoint for." } ], "hidden": true, "description": "Sets breakpoint on AsyncOperation callback handler." }, { "name": "removeAsyncOperationBreakpoint", "parameters": [ { "name": "operationId", "type": "integer", "description": "ID of the async operation to remove breakpoint for." } ], "hidden": true, "description": "Removes AsyncOperation breakpoint." } ], "events": [ { "name": "globalObjectCleared", "description": "Called when global has been cleared and debugger client should reset its state. Happens upon navigation or reload." }, { "name": "scriptParsed", "parameters": [ { "name": "scriptId", "$ref": "ScriptId", "description": "Identifier of the script parsed." }, { "name": "url", "type": "string", "description": "URL or name of the script parsed (if any)." }, { "name": "startLine", "type": "integer", "description": "Line offset of the script within the resource with given URL (for script tags)." }, { "name": "startColumn", "type": "integer", "description": "Column offset of the script within the resource with given URL." }, { "name": "endLine", "type": "integer", "description": "Last line of the script." }, { "name": "endColumn", "type": "integer", "description": "Length of the last line of the script." }, { "name": "isContentScript", "type": "boolean", "optional": true, "description": "Determines whether this script is a user extension script." }, { "name": "isInternalScript", "type": "boolean", "optional": true, "description": "Determines whether this script is an internal script.", "hidden": true }, { "name": "sourceMapURL", "type": "string", "optional": true, "description": "URL of source map associated with script (if any)." }, { "name": "hasSourceURL", "type": "boolean", "optional": true, "description": "True, if this script has sourceURL.", "hidden": true } ], "description": "Fired when virtual machine parses script. This event is also fired for all known and uncollected scripts upon enabling debugger." }, { "name": "scriptFailedToParse", "parameters": [ { "name": "scriptId", "$ref": "ScriptId", "description": "Identifier of the script parsed." }, { "name": "url", "type": "string", "description": "URL or name of the script parsed (if any)." }, { "name": "startLine", "type": "integer", "description": "Line offset of the script within the resource with given URL (for script tags)." }, { "name": "startColumn", "type": "integer", "description": "Column offset of the script within the resource with given URL." }, { "name": "endLine", "type": "integer", "description": "Last line of the script." }, { "name": "endColumn", "type": "integer", "description": "Length of the last line of the script." }, { "name": "isContentScript", "type": "boolean", "optional": true, "description": "Determines whether this script is a user extension script." }, { "name": "isInternalScript", "type": "boolean", "optional": true, "description": "Determines whether this script is an internal script.", "hidden": true }, { "name": "sourceMapURL", "type": "string", "optional": true, "description": "URL of source map associated with script (if any)." }, { "name": "hasSourceURL", "type": "boolean", "optional": true, "description": "True, if this script has sourceURL.", "hidden": true } ], "description": "Fired when virtual machine fails to parse the script." }, { "name": "breakpointResolved", "parameters": [ { "name": "breakpointId", "$ref": "BreakpointId", "description": "Breakpoint unique identifier." }, { "name": "location", "$ref": "Location", "description": "Actual breakpoint location." } ], "description": "Fired when breakpoint is resolved to an actual script and location." }, { "name": "paused", "parameters": [ { "name": "callFrames", "type": "array", "items": { "$ref": "CallFrame" }, "description": "Call stack the virtual machine stopped on." }, { "name": "reason", "type": "string", "enum": [ "XHR", "DOM", "EventListener", "exception", "assert", "CSPViolation", "debugCommand", "promiseRejection", "AsyncOperation", "other" ], "description": "Pause reason." }, { "name": "data", "type": "object", "optional": true, "description": "Object containing break-specific auxiliary properties." }, { "name": "hitBreakpoints", "type": "array", "optional": true, "items": { "type": "string" }, "description": "Hit breakpoints IDs", "hidden": true }, { "name": "asyncStackTrace", "$ref": "StackTrace", "optional": true, "description": "Async stack trace, if any.", "hidden": true } ], "description": "Fired when the virtual machine stopped on breakpoint or exception or any other stop criteria." }, { "name": "resumed", "description": "Fired when the virtual machine resumed execution." }, { "name": "promiseUpdated", "parameters": [ { "name": "eventType", "type": "string", "enum": ["new", "update", "gc"], "description": "Type of the event." }, { "name": "promise", "$ref": "PromiseDetails", "description": "Information about the updated Promise." } ], "description": "Fired when a Promise is created, updated or garbage collected.", "hidden": true }, { "name": "asyncOperationStarted", "parameters": [ { "name": "operation", "$ref": "AsyncOperation", "description": "Information about the async operation." } ], "description": "Fired when an async operation is scheduled (while in a debugger stepping session).", "hidden": true }, { "name": "asyncOperationCompleted", "parameters": [ { "name": "id", "type": "integer", "description": "ID of the async operation that was completed." } ], "description": "Fired when an async operation is completed (while in a debugger stepping session).", "hidden": true } ] }, { "domain": "DOMDebugger", "description": "DOM debugging allows setting breakpoints on particular DOM operations and events. JavaScript execution will stop on these operations as if there was a regular breakpoint set.", "types": [ { "id": "DOMBreakpointType", "type": "string", "enum": ["subtree-modified", "attribute-modified", "node-removed"], "description": "DOM breakpoint type." }, { "id": "EventListener", "type": "object", "description": "Object event listener.", "properties": [ { "name": "type", "type": "string", "description": "EventListener's type." }, { "name": "useCapture", "type": "boolean", "description": "EventListener's useCapture." }, { "name": "location", "$ref": "Debugger.Location", "description": "Handler code location." }, { "name": "handler", "$ref": "Runtime.RemoteObject", "optional": true, "description": "Event handler function value." } ], "hidden": true } ], "commands": [ { "name": "setDOMBreakpoint", "parameters": [ { "name": "nodeId", "$ref": "DOM.NodeId", "description": "Identifier of the node to set breakpoint on." }, { "name": "type", "$ref": "DOMBreakpointType", "description": "Type of the operation to stop upon." } ], "description": "Sets breakpoint on particular operation with DOM." }, { "name": "removeDOMBreakpoint", "parameters": [ { "name": "nodeId", "$ref": "DOM.NodeId", "description": "Identifier of the node to remove breakpoint from." }, { "name": "type", "$ref": "DOMBreakpointType", "description": "Type of the breakpoint to remove." } ], "description": "Removes DOM breakpoint that was set using setDOMBreakpoint." }, { "name": "setEventListenerBreakpoint", "parameters": [ { "name": "eventName", "type": "string", "description": "DOM Event name to stop on (any DOM event will do)." }, { "name": "targetName", "type": "string", "optional": true, "description": "EventTarget interface name to stop on. If equal to \"*\" or not provided, will stop on any EventTarget.", "hidden": true } ], "description": "Sets breakpoint on particular DOM event." }, { "name": "removeEventListenerBreakpoint", "parameters": [ { "name": "eventName", "type": "string", "description": "Event name." }, { "name": "targetName", "type": "string", "optional": true, "description": "EventTarget interface name.", "hidden": true } ], "description": "Removes breakpoint on particular DOM event." }, { "name": "setInstrumentationBreakpoint", "parameters": [ { "name": "eventName", "type": "string", "description": "Instrumentation name to stop on." } ], "description": "Sets breakpoint on particular native event.", "hidden": true }, { "name": "removeInstrumentationBreakpoint", "parameters": [ { "name": "eventName", "type": "string", "description": "Instrumentation name to stop on." } ], "description": "Removes breakpoint on particular native event.", "hidden": true }, { "name": "setXHRBreakpoint", "parameters": [ { "name": "url", "type": "string", "description": "Resource URL substring. All XHRs having this substring in the URL will get stopped upon." } ], "description": "Sets breakpoint on XMLHttpRequest." }, { "name": "removeXHRBreakpoint", "parameters": [ { "name": "url", "type": "string", "description": "Resource URL substring." } ], "description": "Removes breakpoint from XMLHttpRequest." }, { "name": "getEventListeners", "hidden": true, "parameters": [ { "name": "objectId", "$ref": "Runtime.RemoteObjectId", "description": "Identifier of the object to return listeners for." } ], "returns": [ { "name": "listeners", "type": "array", "items": { "$ref": "EventListener" }, "description": "Array of relevant listeners." } ], "description": "Returns event listeners of the given object." } ] }, { "domain": "Profiler", "hidden": true, "types": [ { "id": "CPUProfileNode", "type": "object", "description": "CPU Profile node. Holds callsite information, execution statistics and child nodes.", "properties": [ { "name": "functionName", "type": "string", "description": "Function name." }, { "name": "scriptId", "$ref": "Debugger.ScriptId", "description": "Script identifier." }, { "name": "url", "type": "string", "description": "URL." }, { "name": "lineNumber", "type": "integer", "description": "1-based line number of the function start position." }, { "name": "columnNumber", "type": "integer", "description": "1-based column number of the function start position." }, { "name": "hitCount", "type": "integer", "description": "Number of samples where this node was on top of the call stack." }, { "name": "callUID", "type": "number", "description": "Call UID." }, { "name": "children", "type": "array", "items": { "$ref": "CPUProfileNode" }, "description": "Child nodes." }, { "name": "deoptReason", "type": "string", "description": "The reason of being not optimized. The function may be deoptimized or marked as don't optimize."}, { "name": "id", "type": "integer", "description": "Unique id of the node." }, { "name": "positionTicks", "type": "array", "items": { "$ref": "PositionTickInfo" }, "description": "An array of source position ticks." } ] }, { "id": "CPUProfile", "type": "object", "description": "Profile.", "properties": [ { "name": "head", "$ref": "CPUProfileNode" }, { "name": "startTime", "type": "number", "description": "Profiling start time in seconds." }, { "name": "endTime", "type": "number", "description": "Profiling end time in seconds." }, { "name": "samples", "optional": true, "type": "array", "items": { "type": "integer" }, "description": "Ids of samples top nodes." }, { "name": "timestamps", "optional": true, "type": "array", "items": { "type": "number" }, "description": "Timestamps of the samples in microseconds." } ] }, { "id": "PositionTickInfo", "type": "object", "description": "Specifies a number of samples attributed to a certain source position.", "properties": [ { "name": "line", "type": "integer", "description": "Source line number (1-based)." }, { "name": "ticks", "type": "integer", "description": "Number of samples attributed to the source line." } ] } ], "commands": [ { "name": "enable" }, { "name": "disable" }, { "name": "setSamplingInterval", "parameters": [ { "name": "interval", "type": "integer", "description": "New sampling interval in microseconds." } ], "description": "Changes CPU profiler sampling interval. Must be called before CPU profiles recording started." }, { "name": "start" }, { "name": "stop", "returns": [ { "name": "profile", "$ref": "CPUProfile", "description": "Recorded profile." } ] } ], "events": [ { "name": "consoleProfileStarted", "parameters": [ { "name": "id", "type": "string" }, { "name": "location", "$ref": "Debugger.Location", "description": "Location of console.profile()." }, { "name": "title", "type": "string", "optional": true, "description": "Profile title passed as argument to console.profile()." } ], "description": "Sent when new profile recodring is started using console.profile() call." }, { "name": "consoleProfileFinished", "parameters": [ { "name": "id", "type": "string" }, { "name": "location", "$ref": "Debugger.Location", "description": "Location of console.profileEnd()." }, { "name": "profile", "$ref": "CPUProfile" }, { "name": "title", "type": "string", "optional": true, "description": "Profile title passed as argunet to console.profile()." } ] } ] }, { "domain": "HeapProfiler", "hidden": true, "types": [ { "id": "HeapSnapshotObjectId", "type": "string", "description": "Heap snapshot object id." } ], "commands": [ { "name": "enable" }, { "name": "disable" }, { "name": "startTrackingHeapObjects", "parameters": [ { "name": "trackAllocations", "type": "boolean", "optional": true } ] }, { "name": "stopTrackingHeapObjects", "parameters": [ { "name": "reportProgress", "type": "boolean", "optional": true, "description": "If true 'reportHeapSnapshotProgress' events will be generated while snapshot is being taken when the tracking is stopped." } ] }, { "name": "takeHeapSnapshot", "parameters": [ { "name": "reportProgress", "type": "boolean", "optional": true, "description": "If true 'reportHeapSnapshotProgress' events will be generated while snapshot is being taken." } ] }, { "name": "collectGarbage" }, { "name": "getObjectByHeapObjectId", "parameters": [ { "name": "objectId", "$ref": "HeapSnapshotObjectId" }, { "name": "objectGroup", "type": "string", "optional": true, "description": "Symbolic group name that can be used to release multiple objects." } ], "returns": [ { "name": "result", "$ref": "Runtime.RemoteObject", "description": "Evaluation result." } ] }, { "name": "addInspectedHeapObject", "parameters": [ { "name": "heapObjectId", "$ref": "HeapSnapshotObjectId", "description": "Heap snapshot object id to be accessible by means of $x command line API." } ], "description": "Enables console to refer to the node with given id via $x (see Command Line API for more details $x functions)." }, { "name": "getHeapObjectId", "parameters": [ { "name": "objectId", "$ref": "Runtime.RemoteObjectId", "description": "Identifier of the object to get heap object id for." } ], "returns": [ { "name": "heapSnapshotObjectId", "$ref": "HeapSnapshotObjectId", "description": "Id of the heap snapshot object corresponding to the passed remote object id." } ] } ], "events": [ { "name": "addHeapSnapshotChunk", "parameters": [ { "name": "chunk", "type": "string" } ] }, { "name": "resetProfiles" }, { "name": "reportHeapSnapshotProgress", "parameters": [ { "name": "done", "type": "integer" }, { "name": "total", "type": "integer" }, { "name": "finished", "type": "boolean", "optional": true } ] }, { "name": "lastSeenObjectId", "description": "If heap objects tracking has been started then backend regulary sends a current value for last seen object id and corresponding timestamp. If the were changes in the heap since last event then one or more heapStatsUpdate events will be sent before a new lastSeenObjectId event.", "parameters": [ { "name": "lastSeenObjectId", "type": "integer" }, { "name": "timestamp", "type": "number" } ] }, { "name": "heapStatsUpdate", "description": "If heap objects tracking has been started then backend may send update for one or more fragments", "parameters": [ { "name": "statsUpdate", "type": "array", "items": { "type": "integer" }, "description": "An array of triplets. Each triplet describes a fragment. The first integer is the fragment index, the second integer is a total count of objects for the fragment, the third integer is a total size of the objects for the fragment."} ] } ] }, { "domain": "Worker", "hidden": true, "types": [], "commands": [ { "name": "enable" }, { "name": "disable" }, { "name": "sendMessageToWorker", "parameters": [ { "name": "workerId", "type": "string" }, { "name": "message", "type": "string" } ] }, { "name": "connectToWorker", "parameters": [ { "name": "workerId", "type": "string" } ] }, { "name": "disconnectFromWorker", "parameters": [ { "name": "workerId", "type": "string" } ] }, { "name": "setAutoconnectToWorkers", "parameters": [ { "name": "value", "type": "boolean" } ] } ], "events": [ { "name": "workerCreated", "parameters": [ { "name": "workerId", "type": "string" }, { "name": "url", "type": "string" }, { "name": "inspectorConnected", "type": "boolean" } ] }, { "name": "workerTerminated", "parameters": [ { "name": "workerId", "type": "string" } ] }, { "name": "dispatchMessageFromWorker", "parameters": [ { "name": "workerId", "type": "string" }, { "name": "message", "type": "string" } ] } ] }, { "domain": "ServiceWorker", "hidden": true, "types": [ { "id": "ServiceWorkerRegistration", "type": "object", "description": "ServiceWorker registration.", "properties": [ { "name": "registrationId", "type": "string" }, { "name": "scopeURL", "type": "string" }, { "name": "isDeleted", "type": "boolean", "optional": true } ] }, { "id": "ServiceWorkerVersionRunningStatus", "type": "string", "enum": ["stopped", "starting", "running", "stopping"] }, { "id": "ServiceWorkerVersionStatus", "type": "string", "enum": ["new", "installing", "installed", "activating", "activated", "redundant"] }, { "id": "TargetID", "type": "string" }, { "id": "ServiceWorkerVersion", "type": "object", "description": "ServiceWorker version.", "properties": [ { "name": "versionId", "type": "string" }, { "name": "registrationId", "type": "string" }, { "name": "scriptURL", "type": "string" }, { "name": "runningStatus", "$ref": "ServiceWorkerVersionRunningStatus" }, { "name": "status", "$ref": "ServiceWorkerVersionStatus" }, { "name": "scriptLastModified", "type": "number", "optional": true, "description": "The Last-Modified header value of the main script." }, { "name": "scriptResponseTime", "type": "number", "optional": true, "description": "The time at which the response headers of the main script were received from the server. For cached script it is the last time the cache entry was validated." }, { "name": "controlledClients", "type": "array", "optional": true, "items": { "$ref": "TargetID" } } ] }, { "id": "ServiceWorkerErrorMessage", "type": "object", "description": "ServiceWorker error message.", "properties": [ { "name": "errorMessage", "type": "string" }, { "name": "registrationId", "type": "string" }, { "name": "versionId", "type": "string" }, { "name": "sourceURL", "type": "string" }, { "name": "lineNumber", "type": "integer" }, { "name": "columnNumber", "type": "integer" } ] }, { "id": "TargetInfo", "type": "object", "properties": [ { "name": "id", "$ref": "TargetID" }, { "name": "type", "type": "string" }, { "name": "title", "type": "string" }, { "name": "url", "type": "string" } ] } ], "commands": [ { "name": "enable", "handlers": ["browser"] }, { "name": "disable", "handlers": ["browser"] }, { "name": "sendMessage", "parameters": [ { "name": "workerId", "type": "string" }, { "name": "message", "type": "string" } ], "handlers": ["browser"] }, { "name": "stop", "parameters": [ { "name": "workerId", "type": "string" } ], "handlers": ["browser"] }, { "name": "unregister", "parameters": [ { "name": "scopeURL", "type": "string" } ], "handlers": ["browser"] }, { "name": "updateRegistration", "parameters": [ { "name": "scopeURL", "type": "string" } ], "handlers": ["browser"] }, { "name": "startWorker", "parameters": [ { "name": "scopeURL", "type": "string" } ], "handlers": ["browser"] }, { "name": "stopWorker", "parameters": [ { "name": "versionId", "type": "string" } ], "handlers": ["browser"] }, { "name": "inspectWorker", "parameters": [ { "name": "versionId", "type": "string" } ], "handlers": ["browser"] }, { "name": "skipWaiting", "parameters": [ { "name": "versionId", "type": "string" } ], "handlers": ["browser"] }, { "name": "setDebugOnStart", "parameters": [ { "name": "debugOnStart", "type": "boolean" } ], "handlers": ["browser"] }, { "name": "deliverPushMessage", "parameters": [ { "name": "origin", "type": "string" }, { "name": "registrationId", "type": "string" }, { "name": "data", "type": "string" } ], "handlers": ["browser"] }, { "name": "getTargetInfo", "parameters": [ { "name": "targetId", "$ref": "TargetID" } ], "returns": [ { "name": "targetInfo","$ref": "TargetInfo" } ], "handlers": ["browser"] }, { "name": "activateTarget", "parameters": [ { "name": "targetId", "$ref": "TargetID" } ], "handlers": ["browser"] } ], "events": [ { "name": "workerCreated", "parameters": [ { "name": "workerId", "type": "string" }, { "name": "url", "type": "string" } ], "handlers": ["browser"] }, { "name": "workerTerminated", "parameters": [ { "name": "workerId", "type": "string" } ], "handlers": ["browser"] }, { "name": "dispatchMessage", "parameters": [ { "name": "workerId", "type": "string" }, { "name": "message", "type": "string" } ], "handlers": ["browser"] }, { "name": "workerRegistrationUpdated", "parameters": [ { "name": "registrations", "type": "array", "items": { "$ref": "ServiceWorkerRegistration" } } ], "handlers": ["browser"] }, { "name": "workerVersionUpdated", "parameters": [ { "name": "versions", "type": "array", "items": { "$ref": "ServiceWorkerVersion" } } ], "handlers": ["browser"] }, { "name": "workerErrorReported", "parameters": [ { "name": "errorMessage", "$ref": "ServiceWorkerErrorMessage" } ], "handlers": ["browser"] }, { "name": "debugOnStartUpdated", "parameters": [ { "name": "debugOnStart", "type": "boolean" } ], "handlers": ["browser"] } ] }, { "domain": "Input", "types": [ { "id": "TouchPoint", "type": "object", "hidden": true, "properties": [ { "name": "state", "type": "string", "enum": ["touchPressed", "touchReleased", "touchMoved", "touchStationary", "touchCancelled"], "description": "State of the touch point." }, { "name": "x", "type": "integer", "description": "X coordinate of the event relative to the main frame's viewport."}, { "name": "y", "type": "integer", "description": "Y coordinate of the event relative to the main frame's viewport. 0 refers to the top of the viewport and Y increases as it proceeds towards the bottom of the viewport."}, { "name": "radiusX", "type": "integer", "optional": true, "description": "X radius of the touch area (default: 1)."}, { "name": "radiusY", "type": "integer", "optional": true, "description": "Y radius of the touch area (default: 1)."}, { "name": "rotationAngle", "type": "number", "optional": true, "description": "Rotation angle (default: 0.0)."}, { "name": "force", "type": "number", "optional": true, "description": "Force (default: 1.0)."}, { "name": "id", "type": "number", "optional": true, "description": "Identifier used to track touch sources between events, must be unique within an event."} ] }, { "id": "GestureSourceType", "type": "string", "hidden": true, "enum": ["default", "touch", "mouse"] } ], "commands": [ { "name": "dispatchKeyEvent", "parameters": [ { "name": "type", "type": "string", "enum": ["keyDown", "keyUp", "rawKeyDown", "char"], "description": "Type of the key event." }, { "name": "modifiers", "type": "integer", "optional": true, "description": "Bit field representing pressed modifier keys. Alt=1, Ctrl=2, Meta/Command=4, Shift=8 (default: 0)." }, { "name": "timestamp", "type": "number", "optional": true, "description": "Time at which the event occurred. Measured in UTC time in seconds since January 1, 1970 (default: current time)." }, { "name": "text", "type": "string", "optional": true, "description": "Text as generated by processing a virtual key code with a keyboard layout. Not needed for for keyUp and rawKeyDown events (default: \"\")" }, { "name": "unmodifiedText", "type": "string", "optional": true, "description": "Text that would have been generated by the keyboard if no modifiers were pressed (except for shift). Useful for shortcut (accelerator) key handling (default: \"\")." }, { "name": "keyIdentifier", "type": "string", "optional": true, "description": "Unique key identifier (e.g., 'U+0041') (default: \"\")." }, { "name": "code", "type": "string", "optional": true, "description": "Unique DOM defined string value for each physical key (e.g., 'KeyA') (default: \"\")." }, { "name": "key", "type": "string", "optional": true, "description": "Unique DOM defined string value describing the meaning of the key in the context of active modifiers, keyboard layout, etc (e.g., 'AltGr') (default: \"\")." }, { "name": "windowsVirtualKeyCode", "type": "integer", "optional": true, "description": "Windows virtual key code (default: 0)." }, { "name": "nativeVirtualKeyCode", "type": "integer", "optional": true, "description": "Native virtual key code (default: 0)." }, { "name": "autoRepeat", "type": "boolean", "optional": true, "description": "Whether the event was generated from auto repeat (default: false)." }, { "name": "isKeypad", "type": "boolean", "optional": true, "description": "Whether the event was generated from the keypad (default: false)." }, { "name": "isSystemKey", "type": "boolean", "optional": true, "description": "Whether the event was a system key event (default: false)." } ], "description": "Dispatches a key event to the page.", "handlers": ["browser"] }, { "name": "dispatchMouseEvent", "parameters": [ { "name": "type", "type": "string", "enum": ["mousePressed", "mouseReleased", "mouseMoved"], "description": "Type of the mouse event." }, { "name": "x", "type": "integer", "description": "X coordinate of the event relative to the main frame's viewport."}, { "name": "y", "type": "integer", "description": "Y coordinate of the event relative to the main frame's viewport. 0 refers to the top of the viewport and Y increases as it proceeds towards the bottom of the viewport."}, { "name": "modifiers", "type": "integer", "optional": true, "description": "Bit field representing pressed modifier keys. Alt=1, Ctrl=2, Meta/Command=4, Shift=8 (default: 0)." }, { "name": "timestamp", "type": "number", "optional": true, "description": "Time at which the event occurred. Measured in UTC time in seconds since January 1, 1970 (default: current time)." }, { "name": "button", "type": "string", "enum": ["none", "left", "middle", "right"], "optional": true, "description": "Mouse button (default: \"none\")." }, { "name": "clickCount", "type": "integer", "optional": true, "description": "Number of times the mouse button was clicked (default: 0)." } ], "description": "Dispatches a mouse event to the page.", "handlers": ["browser"] }, { "name": "dispatchTouchEvent", "hidden": true, "parameters": [ { "name": "type", "type": "string", "enum": ["touchStart", "touchEnd", "touchMove"], "description": "Type of the touch event." }, { "name": "touchPoints", "type": "array", "items": { "$ref": "TouchPoint" }, "description": "Touch points." }, { "name": "modifiers", "type": "integer", "optional": true, "description": "Bit field representing pressed modifier keys. Alt=1, Ctrl=2, Meta/Command=4, Shift=8 (default: 0)." }, { "name": "timestamp", "type": "number", "optional": true, "description": "Time at which the event occurred. Measured in UTC time in seconds since January 1, 1970 (default: current time)." } ], "description": "Dispatches a touch event to the page." }, { "name": "emulateTouchFromMouseEvent", "hidden": true, "parameters": [ { "name": "type", "type": "string", "enum": ["mousePressed", "mouseReleased", "mouseMoved", "mouseWheel"], "description": "Type of the mouse event." }, { "name": "x", "type": "integer", "description": "X coordinate of the mouse pointer in DIP."}, { "name": "y", "type": "integer", "description": "Y coordinate of the mouse pointer in DIP."}, { "name": "timestamp", "type": "number", "description": "Time at which the event occurred. Measured in UTC time in seconds since January 1, 1970." }, { "name": "button", "type": "string", "enum": ["none", "left", "middle", "right"], "description": "Mouse button." }, { "name": "deltaX", "type": "number", "optional": true, "description": "X delta in DIP for mouse wheel event (default: 0)."}, { "name": "deltaY", "type": "number", "optional": true, "description": "Y delta in DIP for mouse wheel event (default: 0)."}, { "name": "modifiers", "type": "integer", "optional": true, "description": "Bit field representing pressed modifier keys. Alt=1, Ctrl=2, Meta/Command=4, Shift=8 (default: 0)." }, { "name": "clickCount", "type": "integer", "optional": true, "description": "Number of times the mouse button was clicked (default: 0)." } ], "description": "Emulates touch event from the mouse event parameters.", "handlers": ["browser"] }, { "name": "synthesizePinchGesture", "async": true, "parameters": [ { "name": "x", "type": "integer", "description": "X coordinate of the start of the gesture in CSS pixels." }, { "name": "y", "type": "integer", "description": "Y coordinate of the start of the gesture in CSS pixels." }, { "name": "scaleFactor", "type": "number", "description": "Relative scale factor after zooming (>1.0 zooms in, <1.0 zooms out)." }, { "name": "relativeSpeed", "type": "integer", "optional": true, "description": "Relative pointer speed in pixels per second (default: 800)." }, { "name": "gestureSourceType", "$ref": "GestureSourceType", "optional": true, "description": "Which type of input events to be generated (default: 'default', which queries the platform for the preferred input type)." } ], "description": "Synthesizes a pinch gesture over a time period by issuing appropriate touch events.", "hidden": true, "handlers": ["browser"] }, { "name": "synthesizeScrollGesture", "async": true, "parameters": [ { "name": "x", "type": "integer", "description": "X coordinate of the start of the gesture in CSS pixels." }, { "name": "y", "type": "integer", "description": "Y coordinate of the start of the gesture in CSS pixels." }, { "name": "xDistance", "type": "integer", "optional": true, "description": "The distance to scroll along the X axis (positive to scroll left)." }, { "name": "yDistance", "type": "integer", "optional": true, "description": "The distance to scroll along the Y axis (positive to scroll up)." }, { "name": "xOverscroll", "type": "integer", "optional": true, "description": "The number of additional pixels to scroll back along the X axis, in addition to the given distance." }, { "name": "yOverscroll", "type": "integer", "optional": true, "description": "The number of additional pixels to scroll back along the Y axis, in addition to the given distance." }, { "name": "preventFling", "type": "boolean", "optional": true, "description": "Prevent fling (default: true)." }, { "name": "speed", "type": "integer", "optional": true, "description": "Swipe speed in pixels per second (default: 800)." }, { "name": "gestureSourceType", "$ref": "GestureSourceType", "optional": true, "description": "Which type of input events to be generated (default: 'default', which queries the platform for the preferred input type)." } ], "description": "Synthesizes a scroll gesture over a time period by issuing appropriate touch events.", "hidden": true, "handlers": ["browser"] }, { "name": "synthesizeTapGesture", "async": true, "parameters": [ { "name": "x", "type": "integer", "description": "X coordinate of the start of the gesture in CSS pixels." }, { "name": "y", "type": "integer", "description": "Y coordinate of the start of the gesture in CSS pixels." }, { "name": "duration", "type": "integer", "optional": true, "description": "Duration between touchdown and touchup events in ms (default: 50)." }, { "name": "tapCount", "type": "integer", "optional": true, "description": "Number of times to perform the tap (e.g. 2 for double tap, default: 1)." }, { "name": "gestureSourceType", "$ref": "GestureSourceType", "optional": true, "description": "Which type of input events to be generated (default: 'default', which queries the platform for the preferred input type)." } ], "description": "Synthesizes a tap gesture over a time period by issuing appropriate touch events.", "hidden": true, "handlers": ["browser"] } ], "events": [] }, { "domain": "LayerTree", "hidden": true, "types": [ { "id": "LayerId", "type": "string", "description": "Unique Layer identifier." }, { "id": "SnapshotId", "type": "string", "description": "Unique snapshot identifier." }, { "id": "ScrollRect", "type": "object", "description": "Rectangle where scrolling happens on the main thread.", "properties": [ { "name": "rect", "$ref": "DOM.Rect", "description": "Rectangle itself." }, { "name": "type", "type": "string", "enum": ["RepaintsOnScroll", "TouchEventHandler", "WheelEventHandler"], "description": "Reason for rectangle to force scrolling on the main thread" } ] }, { "id": "PictureTile", "type": "object", "description": "Serialized fragment of layer picture along with its offset within the layer.", "properties": [ { "name": "x", "type": "number", "description": "Offset from owning layer left boundary" }, { "name": "y", "type": "number", "description": "Offset from owning layer top boundary" }, { "name": "picture", "type": "string", "description": "Base64-encoded snapshot data." } ] }, { "id": "Layer", "type": "object", "description": "Information about a compositing layer.", "properties": [ { "name": "layerId", "$ref": "LayerId", "description": "The unique id for this layer." }, { "name": "parentLayerId", "$ref": "LayerId", "optional": true, "description": "The id of parent (not present for root)." }, { "name": "backendNodeId", "$ref": "DOM.BackendNodeId", "optional": true, "description": "The backend id for the node associated with this layer." }, { "name": "offsetX", "type": "number", "description": "Offset from parent layer, X coordinate." }, { "name": "offsetY", "type": "number", "description": "Offset from parent layer, Y coordinate." }, { "name": "width", "type": "number", "description": "Layer width." }, { "name": "height", "type": "number", "description": "Layer height." }, { "name": "transform", "type": "array", "items": { "type": "number" }, "minItems": 16, "maxItems": 16, "optional": true, "description": "Transformation matrix for layer, default is identity matrix" }, { "name": "anchorX", "type": "number", "optional": true, "description": "Transform anchor point X, absent if no transform specified" }, { "name": "anchorY", "type": "number", "optional": true, "description": "Transform anchor point Y, absent if no transform specified" }, { "name": "anchorZ", "type": "number", "optional": true, "description": "Transform anchor point Z, absent if no transform specified" }, { "name": "paintCount", "type": "integer", "description": "Indicates how many time this layer has painted." }, { "name": "drawsContent", "type": "boolean", "description": "Indicates whether this layer hosts any content, rather than being used for transform/scrolling purposes only." }, { "name": "invisible", "type": "boolean", "optional": true, "description": "Set if layer is not visible." }, { "name": "scrollRects", "type": "array", "items": { "$ref": "ScrollRect"}, "optional": true, "description": "Rectangles scrolling on main thread only."} ] }, { "id": "PaintProfile", "type": "array", "description": "Array of timings, one per paint step.", "items": { "type": "number", "description": "A time in seconds since the end of previous step (for the first step, time since painting started)" } } ], "commands": [ { "name": "enable", "description": "Enables compositing tree inspection." }, { "name": "disable", "description": "Disables compositing tree inspection." }, { "name": "compositingReasons", "parameters": [ { "name": "layerId", "$ref": "LayerId", "description": "The id of the layer for which we want to get the reasons it was composited." } ], "description": "Provides the reasons why the given layer was composited.", "returns": [ { "name": "compositingReasons", "type": "array", "items": { "type": "string" }, "description": "A list of strings specifying reasons for the given layer to become composited." } ] }, { "name": "makeSnapshot", "parameters": [ { "name": "layerId", "$ref": "LayerId", "description": "The id of the layer." } ], "description": "Returns the layer snapshot identifier.", "returns": [ { "name": "snapshotId", "$ref": "SnapshotId", "description": "The id of the layer snapshot." } ] }, { "name": "loadSnapshot", "parameters": [ { "name": "tiles", "type": "array", "items": { "$ref": "PictureTile" }, "minItems": 1, "description": "An array of tiles composing the snapshot." } ], "description": "Returns the snapshot identifier.", "returns": [ { "name": "snapshotId", "$ref": "SnapshotId", "description": "The id of the snapshot." } ] }, { "name": "releaseSnapshot", "parameters": [ { "name": "snapshotId", "$ref": "SnapshotId", "description": "The id of the layer snapshot." } ], "description": "Releases layer snapshot captured by the back-end." }, { "name": "profileSnapshot", "parameters": [ { "name": "snapshotId", "$ref": "SnapshotId", "description": "The id of the layer snapshot." }, { "name": "minRepeatCount", "type": "integer", "optional": true, "description": "The maximum number of times to replay the snapshot (1, if not specified)." }, { "name": "minDuration", "type": "number", "optional": true, "description": "The minimum duration (in seconds) to replay the snapshot." }, { "name": "clipRect", "$ref": "DOM.Rect", "optional": true, "description": "The clip rectangle to apply when replaying the snapshot." } ], "returns": [ { "name": "timings", "type": "array", "items": { "$ref": "PaintProfile" }, "description": "The array of paint profiles, one per run." } ] }, { "name": "replaySnapshot", "parameters": [ { "name": "snapshotId", "$ref": "SnapshotId", "description": "The id of the layer snapshot." }, { "name": "fromStep", "type": "integer", "optional": true, "description": "The first step to replay from (replay from the very start if not specified)." }, { "name": "toStep", "type": "integer", "optional": true, "description": "The last step to replay to (replay till the end if not specified)." }, { "name": "scale", "type": "number", "optional": true, "description": "The scale to apply while replaying (defaults to 1)." } ], "description": "Replays the layer snapshot and returns the resulting bitmap.", "returns": [ { "name": "dataURL", "type": "string", "description": "A data: URL for resulting image." } ] }, { "name": "snapshotCommandLog", "parameters": [ { "name": "snapshotId", "$ref": "SnapshotId", "description": "The id of the layer snapshot." } ], "description": "Replays the layer snapshot and returns canvas log.", "returns": [ { "name": "commandLog", "type": "array", "items": { "type": "object" }, "description": "The array of canvas function calls." } ] } ], "events": [ { "name": "layerTreeDidChange", "parameters": [ { "name": "layers", "type": "array", "items": { "$ref": "Layer" }, "optional": true, "description": "Layer tree, absent if not in the comspositing mode." } ] }, { "name": "layerPainted", "parameters": [ { "name": "layerId", "$ref": "LayerId", "description": "The id of the painted layer." }, { "name": "clip", "$ref": "DOM.Rect", "description": "Clip rectangle." } ] } ] }, { "domain": "DeviceOrientation", "hidden": true, "commands": [ { "name": "setDeviceOrientationOverride", "description": "Overrides the Device Orientation.", "parameters": [ { "name": "alpha", "type": "number", "description": "Mock alpha"}, { "name": "beta", "type": "number", "description": "Mock beta"}, { "name": "gamma", "type": "number", "description": "Mock gamma"} ] }, { "name": "clearDeviceOrientationOverride", "description": "Clears the overridden Device Orientation." } ] }, { "domain": "ScreenOrientation", "hidden": true, "types": [ { "id": "OrientationType", "type": "string", "enum": ["portraitPrimary", "portraitSecondary", "landscapePrimary", "landscapeSecondary"], "description": "Orientation type" } ], "commands": [ { "name": "setScreenOrientationOverride", "description": "Overrides the Screen Orientation.", "parameters": [ { "name": "angle", "type": "integer", "description": "Orientation angle" }, { "name": "type", "$ref": "OrientationType", "description": "Orientation type" } ] }, { "name": "clearScreenOrientationOverride", "description": "Clears the overridden Screen Orientation." } ] }, { "domain": "Tracing", "commands": [ { "name": "start", "async": true, "description": "Start trace events collection.", "parameters": [ { "name": "categories", "type": "string", "optional": true, "description": "Category/tag filter" }, { "name": "options", "type": "string", "optional": true, "description": "Tracing options" }, { "name": "bufferUsageReportingInterval", "type": "number", "optional": true, "description": "If set, the agent will issue bufferUsage events at this interval, specified in milliseconds" } ], "handlers": ["browser", "renderer"] }, { "name": "end", "async": true, "description": "Stop trace events collection.", "handlers": ["browser", "renderer"] }, { "name": "getCategories", "async": true, "description": "Gets supported tracing categories.", "returns": [ { "name": "categories", "type": "array", "items": { "type": "string" }, "description": "A list of supported tracing categories." } ], "handlers": ["browser"] } ], "events": [ { "name": "dataCollected", "parameters": [ { "name": "value", "type": "array", "items": { "type": "object" } } ], "description": "Contains an bucket of collected trace events. When tracing is stopped collected events will be send as a sequence of dataCollected events followed by tracingComplete event.", "handlers": ["browser"] }, { "name": "tracingComplete", "description": "Signals that tracing is stopped and there is no trace buffers pending flush, all data were delivered via dataCollected events.", "handlers": ["browser"] }, { "name": "bufferUsage", "parameters": [ { "name": "percentFull", "type": "number", "optional": true, "description": "A number in range [0..1] that indicates the used size of event buffer as a fraction of its total size." }, { "name": "eventCount", "type": "number", "optional": true, "description": "An approximate number of events in the trace log." }, { "name": "value", "type": "number", "optional": true, "description": "A number in range [0..1] that indicates the used size of event buffer as a fraction of its total size." } ], "handlers": ["browser"] } ] }, { "domain": "Power", "hidden": true, "types": [ { "id": "PowerEvent", "type": "object", "properties": [ { "name": "type", "type": "string", "description": "Power Event Type." }, { "name": "timestamp", "type": "number", "description": "Power Event Time, in milliseconds." }, { "name": "value", "type": "number", "description": "Power Event Value." } ], "description": "PowerEvent item" } ], "commands": [ { "name": "start", "description": "Start power events collection.", "parameters": [], "handlers": ["browser", "frontend"] }, { "name": "end", "description": "Stop power events collection.", "parameters": [], "handlers": ["browser", "frontend"] }, { "name": "canProfilePower", "description": "Tells whether power profiling is supported.", "returns": [ { "name": "result", "type": "boolean", "description": "True if power profiling is supported." } ], "hidden": true, "handlers": ["browser", "frontend"] }, { "name": "getAccuracyLevel", "description": "Describes the accuracy level of the data provider.", "returns": [ { "name": "result", "type": "string", "enum": ["high", "moderate", "low"] } ], "handlers": ["browser", "frontend"] } ], "events": [ { "name": "dataAvailable", "parameters": [ {"name": "value", "type": "array", "items": { "$ref": "PowerEvent" }, "description": "List of power events." } ], "handlers": ["browser", "frontend"] } ] }, { "domain": "Animation", "hidden": true, "types": [ { "id": "Animation", "type": "object", "hidden": true, "properties": [ { "name": "id", "type": "string", "description": "Animation's id." }, { "name": "pausedState", "type": "boolean", "hidden": "true", "description": "Animation's internal paused state." }, { "name": "playState", "type": "string", "description": "Animation's play state." }, { "name": "playbackRate", "type": "number", "description": "Animation's playback rate." }, { "name": "startTime", "type": "number", "description": "Animation's start time." }, { "name": "currentTime", "type": "number", "description": "Animation's current time." }, { "name": "source", "$ref": "AnimationEffect", "description": "Animation's source animation node." }, { "name": "type", "type": "string", "enum": ["CSSTransition", "CSSAnimation", "WebAnimation"], "description": "Animation type of Animation." } ], "description": "Animation instance." }, { "id": "AnimationEffect", "type": "object", "hidden": true, "properties": [ { "name": "delay", "type": "number", "description": "AnimationEffect's delay." }, { "name": "endDelay", "type": "number", "description": "AnimationEffect's end delay." }, { "name": "playbackRate", "type": "number", "description": "AnimationEffect's playbackRate." }, { "name": "iterationStart", "type": "number", "description": "AnimationEffect's iteration start." }, { "name": "iterations", "type": "number", "description": "AnimationEffect's iterations." }, { "name": "duration", "type": "number", "description": "AnimationEffect's iteration duration." }, { "name": "direction", "type": "string", "description": "AnimationEffect's playback direction." }, { "name": "fill", "type": "string", "description": "AnimationEffect's fill mode." }, { "name": "name", "type": "string", "description": "AnimationEffect's name." }, { "name": "backendNodeId", "$ref": "DOM.BackendNodeId", "description": "AnimationEffect's target node." }, { "name": "keyframesRule", "$ref": "KeyframesRule", "optional": true, "description": "AnimationEffect's keyframes." }, { "name": "easing", "type": "string", "description": "AnimationEffect's timing function." } ], "description": "AnimationEffect instance" }, { "id": "KeyframesRule", "type": "object", "properties": [ { "name": "name", "type": "string", "optional": true, "description": "CSS keyframed animation's name." }, { "name": "keyframes", "type": "array", "items": { "$ref": "KeyframeStyle" }, "description": "List of animation keyframes." } ], "description": "Keyframes Rule" }, { "id": "KeyframeStyle", "type": "object", "properties": [ { "name": "offset", "type": "string", "description": "Keyframe's time offset." }, { "name": "easing", "type": "string", "description": "AnimationEffect's timing function." } ], "description": "Keyframe Style" } ], "commands": [ { "name": "enable", "description": "Enables animation domain notifications." }, { "name": "disable", "description": "Disables animation domain notifications." }, { "name": "getPlaybackRate", "returns": [ { "name": "playbackRate", "type": "number", "description": "Playback rate for animations on page."} ], "description": "Gets the playback rate of the document timeline." }, { "name": "setPlaybackRate", "parameters": [ { "name": "playbackRate", "type": "number", "description": "Playback rate for animations on page" } ], "description": "Sets the playback rate of the document timeline." }, { "name": "setCurrentTime", "parameters": [ { "name": "currentTime", "type": "number", "description": "Current time for the page animation timeline" } ], "description": "Sets the current time of the document timeline." }, { "name": "setTiming", "parameters": [ { "name": "playerId", "type": "string", "description": "AnimationPlayer id." }, { "name": "duration", "type": "number", "description": "Duration of the animation." }, { "name": "delay", "type": "number", "description": "Delay of the animation." } ], "description": "Sets the timing of an animation node." } ], "events": [ { "name": "animationCreated", "parameters": [ { "name": "player", "$ref": "Animation", "description": "Animation that was created." }, { "name": "resetTimeline", "type": "boolean", "description": "Whether the timeline should be reset." } ], "description": "Event for each animation player that has been created." }, { "name": "animationCanceled", "parameters": [ { "name": "id", "type": "string", "description": "Id of the Animation that was cancelled." } ], "description": "Event for Animations in the frontend that have been cancelled." } ] }, { "domain": "Accessibility", "hidden": true, "types": [ { "id": "AXNodeId", "type": "string", "description": "Unique accessibility node identifier." }, { "id": "AXValueType", "type": "string", "enum": [ "boolean", "tristate", "booleanOrUndefined", "idref", "idrefList", "integer", "number", "string", "token", "tokenList", "domRelation", "role", "internalRole" ], "description": "Enum of possible property types." }, { "id": "AXPropertySourceType", "type": "string", "enum": [ "attribute", "implicit", "style" ], "description": "Enum of possible property sources." }, { "id": "AXPropertySource", "type": "object", "properties": [ { "name": "name", "type": "string", "description": "The name/label of this source." }, { "name": "sourceType", "$ref": "AXPropertySourceType", "description": "What type of source this is." }, { "name": "value", "type": "any", "description": "The value of this property source." }, { "name": "type", "$ref": "AXValueType", "description": "What type the value should be interpreted as." }, { "name": "invalid", "type": "boolean", "description": "Whether the value for this property is invalid.", "optional": true }, { "name": "invalidReason", "type": "string", "description": "Reason for the value being invalid, if it is.", "optional": true } ], "description": "A single source for a computed AX property." }, { "id": "AXRelatedNode", "type": "object", "properties": [ { "name": "idref", "type": "string", "description": "The IDRef value provided, if any.", "optional": true }, { "name": "backendNodeId", "$ref": "DOM.BackendNodeId", "description": "The BackendNodeId of the related node." } ] }, { "id": "AXProperty", "type": "object", "properties": [ { "name": "name", "type": "string", "description": "The name of this property." }, { "name": "value", "$ref": "AXValue", "description": "The value of this property." } ] }, { "id": "AXValue", "type": "object", "properties": [ { "name": "type", "$ref": "AXValueType", "description": "The type of this value." }, { "name": "value", "type": "any", "description": "The computed value of this property.", "optional": true }, { "name": "relatedNodeValue", "$ref": "AXRelatedNode", "description": "The related node value, if any.", "optional": true }, { "name": "relatedNodeArrayValue", "type": "array", "items": { "$ref": "AXRelatedNode" }, "description": "Multiple relted nodes, if applicable.", "optional": true }, { "name": "sources", "type": "array", "items": { "$ref": "AXPropertySource" }, "description": "The sources which contributed to the computation of this property.", "optional": true } ], "description": "A single computed AX property." }, { "id": "AXGlobalStates", "type": "string", "enum": [ "disabled", "hidden", "hiddenRoot", "invalid" ], "description": "States which apply to every AX node." }, { "id": "AXLiveRegionAttributes", "type": "string", "enum": [ "live", "atomic", "relevant", "busy", "root" ], "description": "Attributes which apply to nodes in live regions." }, { "id": "AXWidgetAttributes", "type": "string", "enum": [ "autocomplete", "haspopup", "level", "multiselectable", "orientation", "multiline", "readonly", "required", "valuemin", "valuemax", "valuetext" ], "Description": "Attributes which apply to widgets." }, { "id": "AXWidgetStates", "type": "string", "enum": [ "checked", "expanded", "pressed", "selected" ], "description": "States which apply to widgets." }, { "id": "AXRelationshipAttributes", "type": "string", "enum": [ "activedescendant", "flowto", "controls", "describedby", "labelledby", "owns" ], "description": "Relationships between elements other than parent/child/sibling." }, { "id": "AXNode", "type": "object", "properties": [ { "name": "nodeId", "$ref": "AXNodeId", "description": "Unique identifier for this node." }, { "name": "ignored", "type": "boolean", "description": "Whether this node is ignored for accessibility" }, { "name": "ignoredReasons", "type": "array", "items": { "$ref": "AXProperty" }, "description": "Collection of reasons why this node is hidden.", "optional": true }, { "name": "role", "$ref": "AXValue", "description": "This Node's role, whether explicit or implicit.", "optional": true}, { "name": "name", "$ref": "AXValue", "description": "The accessible name for this Node.", "optional": true }, { "name": "description", "$ref": "AXValue", "description": "The accessible description for this Node.", "optional": true }, { "name": "value", "$ref": "AXValue", "description": "The value for this Node.", "optional": true }, { "name": "help", "$ref": "AXValue", "description": "Help.", "optional": true }, { "name": "properties", "type": "array", "items": { "$ref": "AXProperty" }, "description": "All other properties", "optional": true } ], "description": "A node in the accessibility tree." } ], "commands": [ { "name": "getAXNode", "parameters": [ { "name": "nodeId", "$ref": "DOM.NodeId", "description": "ID of node to get accessibility node for." } ], "returns": [ { "name": "accessibilityNode", "$ref": "AXNode", "description": "The Accessibility.AXNode for this DOM node, if it exists.", "optional": true } ], "description": "Fetches the accessibility node for this DOM node, if it exists.", "hidden": true } ] }, { "domain": "Security", "description": "Security", "hidden": true, "types": [ { "id": "SecurityState", "type": "string", "enum": ["unknown", "http", "insecure", "warning", "secure"], "description": "The security level of a page or resource." }, { "id": "SecurityStateExplanation", "type": "object", "properties": [ { "name": "securityState", "$ref": "SecurityState", "description": "Security state representing the severity of the factor being explained." }, { "name": "summary", "type": "string", "description": "Short phrase describing the type of factor." }, { "name": "description", "type": "string", "description": "Full text explanation of the factor." } ], "description": "An explanation of an factor contributing to the security state." } ], "commands": [ { "name": "enable", "description": "Enables tracking security state changes.", "handlers": ["browser"] }, { "name": "disable", "description": "Disables tracking security state changes.", "handlers": ["browser"] } ], "events": [ { "name": "securityStateChanged", "description": "The security state of the page changed.", "parameters": [ { "name": "securityState", "$ref": "SecurityState", "description": "Security state." }, { "name": "explanations", "type": "array", "items": { "$ref": "SecurityStateExplanation" }, "description": "List of explanations for the security state. If the overall security state is `insecure` or `warning`, at least one corresponding explanation should be included.", "optional": true } ], "handlers": ["browser"] } ] }] } ================================================ FILE: build-tools/readme.md ================================================ #scraper Example: ``` ➜ node scraper.js protocol.json Debugger.FunctionDetails ``` ... generates the `FunctionDetails` object defined in the `Debugger` namespace using the file `protocol.json`, which is downloaded from google code. Note: if not specified, the domain is implicit. Here's the code that was generated: ``` public static class FunctionDetails { @JsonProperty public Location location; @JsonProperty(required = true) public String functionName; @JsonProperty(required = true) public boolean isGenerator; @JsonProperty public List scopeChain; } public static class Scope { @JsonProperty(required = true) public String type; @JsonProperty(required = true) public Runtime.RemoteObject object; } ... and so on (omited for brevity) ``` ================================================ FILE: build-tools/scraper.js ================================================ var fs = require('fs'); var dependencies = []; var alreadyGenerated = []; var anonymousTypesToGenerate = []; var anonymousTypeCount = 0; var primitiveObjectTypes = { 'string': 'String', 'boolean': 'boolean', 'integer': 'int', 'number': 'double', 'any': 'Object' }; var primitiveOptionalObjectTypes = { 'string': 'String', 'boolean': 'Boolean', 'integer': 'Integer', 'number': 'Double', 'any': 'Object' }; function tab(amount) { amount = amount || 1; // two spaces per indent return new Array(amount + 1).join(' '); } function ret(amount) { amount = amount || 1; return new Array(amount + 1).join('\r\n'); } function getAnonymousTypeName() { return 'AnonType' + anonymousTypeCount++; } function isPrimitive(typeName) { return primitiveObjectTypes.hasOwnProperty(typeName); } function isPrimitiveOrArray(typeName) { return isPrimitive(typeName) || typeName == 'array'; } function resolveName(name) { var containsDot = name.indexOf('.') >= 0; var split = name.split('.'); return { domain: containsDot ? split[0] : '', name: containsDot ? split[1] : name }; } function findCommandOrEventDefinition(resolved) { var match = null; documentation.domains.forEach(function (domain) { if (!resolved.domain || resolved.domain == domain.domain) { if (domain.types) { var filterFunc = function(commandOrEvent) { return commandOrEvent.name == resolved.name; }; var matches = domain.commands.filter(filterFunc); matches = matches.concat(domain.events.filter(filterFunc)); if (matches.length > 0) { match = matches[0]; resolved.domain = domain.domain; } } } }); return match; } function findTypeDefinition(resolved) { var match = null; documentation.domains.forEach(function (domain) { if (!resolved.domain || resolved.domain == domain.domain) { if (domain.types) { var matches = domain.types.filter(function (type) { return type.id == resolved.name; }); if (matches.length > 0) { match = matches[0]; resolved.domain = domain.domain; } } } }); return match; } function generateJavaTypeEquivalent(currentType, prop) { if (prop.hasOwnProperty('type')) { // if it's a primitive type, then just map to the java equivalent var type = primitiveObjectTypes.hasOwnProperty(prop.type) ? primitiveObjectTypes[prop.type] : prop.type; // if the property is optional, it is nullable if (prop.optional && primitiveOptionalObjectTypes.hasOwnProperty(prop.type)) { type = primitiveOptionalObjectTypes[prop.type]; } if (prop.type == 'array') { if (prop.items.hasOwnProperty('$ref')) { if (currentType != prop.items.$ref) { addDependencyIfNotGenerated(prop.items.$ref); } type = 'List<' + prop.items.$ref + '>'; } else { type = getAnonymousTypeName(); var typeDef = { id: type, type: prop.items.type, properties: prop.items.properties }; type = 'List<' + type + '>'; anonymousTypesToGenerate.push(typeDef); } } return type; } else { var typeDefinition = findTypeDefinition(resolveName(prop.$ref)); if (!isPrimitiveOrArray(typeDefinition.type)) { if (typeDefinition.type != currentType) { addDependencyIfNotGenerated(prop.$ref); } return prop.$ref; } else { var type = typeDefinition.type; var resolvedType = primitiveObjectTypes[type] || type; if (prop.optional) { if (primitiveOptionalObjectTypes.hasOwnProperty(type)) { resolvedType = primitiveOptionalObjectTypes[type]; } } if (type == 'array') { addDependencyIfNotGenerated(typeDefinition.items.$ref); return 'List<' + typeDefinition.items.$ref + '>'; } return resolvedType; } } } function generateJavaClassForType(typeDefinition) { var result = 'public static class ' + typeDefinition.id + ' {'; if (typeDefinition.properties != undefined) { typeDefinition.properties.forEach(function (prop) { result += ret() + tab() + '\@JsonProperty'; if (!prop.optional) { result += '(required = true)'; } result += ret(); result += tab() + 'public ' + generateJavaTypeEquivalent(typeDefinition.id, prop) + ' ' + prop.name + ';' + ret(); }); } else { result += ret(); } result += '}'; return result; } function addDependencyIfNotGenerated(dependencyString) { var resolved = resolveName(dependencyString); if (!dependencyExists(resolved) && !isPrimitiveOrArray(findTypeDefinition(resolved).type)) { dependencies.push(resolved); } } function dependencyExists(resolvedDependency) { return dependencies.filter(function (existing) { return resolvedDependency.name == existing.name && resolvedDependency.domain == existing.domain; }).length != 0 || alreadyGenerated.filter(function (existing) { return resolvedDependency.name == existing.name && resolvedDependency.domain == existing.domain; }).length != 0; } function generateCommandOrEvent(commandDef) { var className = commandDef.name.charAt(0).toUpperCase() + commandDef.name.slice(1); var hasParams = !!commandDef.parameters; var hasReturns = !!commandDef.returns; var paramsTypeName = className + 'Request'; var returnsTypeName = className + 'Response'; var result = '' + '@ChromeDevtoolsMethod' + ret() + 'public JsonRpcResult ' + commandDef.name + '(JsonRpcPeer peer, JSONObject params) {' + ret(); if (hasParams) { result += '' + tab(1) + 'final ' + paramsTypeName + ' = mObjectMapper.convertValue' + ret() + tab(2) + 'params,' + ret() + tab(2) + paramsTypeName + '.type);' + ret(2); } if (hasReturns) { result += '' + tab() + 'final ' + returnsTypeName + 'response = new ' + returnsTypeName + '();' + ret() + tab() + 'return response;' + ret(); } result += '}' + ret(2); if (hasParams) { result += generateType({ id: paramsTypeName, properties: commandDef.parameters }) + ret(2); } if (hasReturns) { result += generateType({ id: returnsTypeName, properties: commandDef.returns }); } return result; } function generateDependencies() { var result = ''; while (dependencies.length > 0 || anonymousTypesToGenerate.length > 0) { while (dependencies.length > 0) { result += ret(2) + generateType( findTypeDefinition( dependencies.pop())); } while (anonymousTypesToGenerate.length > 0) { result += ret(2) + generateType(anonymousTypesToGenerate.pop()); } } return result; } function generateType(typeDef) { alreadyGenerated.push(resolveName(typeDef.id)); if (isPrimitiveOrArray(typeDef.type)) { return 'The type \'' + typeDef.id + '\' is a primitive type (' + typeDef.type + '), so no class generation is necessary.'; } var result = generateJavaClassForType(typeDef); result += generateDependencies(); return result; } function generate(name) { var resolved = resolveName(name); var command = findCommandOrEventDefinition(resolved); if (command != null) { return generateCommandOrEvent(command); } var type = findTypeDefinition(resolved); if (type != null) { return generateType(type); } return 'no command or type \'' + name + '\' found'; } // first two args are path to node and to this file var arguments = process.argv.slice(2); if (arguments.length == 0) { console.log('usage:' + ret() + tab() + 'node scraper.js path_to_protocol_json name_of_method_or_type' + ret() + ' node scraper.js path_to_protocol_json domain.name_of_method_or_type' + ret() + 'description:' + ret() + tab() + 'This script generates Java code representing a type or method defined in `protocol.json`,' + ' which can be found at: https://code.google.com/p/chromium/codesearch#chromium/src/third_party/WebKit/Source/devtools/protocol.json') } else { var documentation = JSON.parse(fs.readFileSync(arguments[0], {encoding: 'utf8'})); console.log(generate(arguments[1])); } ================================================ FILE: build.gradle ================================================ buildscript { repositories { google() jcenter() } dependencies { classpath 'com.android.tools.build:gradle:4.1.2' } } allprojects { repositories { google() jcenter() } } ext { compileSdkVersion = 30 targetSdkVersion = 30 } ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ #Mon Mar 08 22:57:34 PST 2021 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip ================================================ FILE: gradle.properties ================================================ # Project-wide Gradle settings. # IDE (e.g. Android Studio) users: # Gradle settings configured through the IDE *will override* # any settings specified in this file. # For more details on how to configure your build environment visit # http://www.gradle.org/docs/current/userguide/build_environment.html # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. org.gradle.jvmargs=-Xmx1536m # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true android.useAndroidX=true VERSION_NAME=1.6.1-SNAPSHOT GROUP=com.facebook.stetho ================================================ FILE: gradlew ================================================ #!/usr/bin/env sh ############################################################################## ## ## Gradle start up script for UN*X ## ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" # Need this for relative symlinks. while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG=`dirname "$PRG"`"/$link" fi done SAVED="`pwd`" cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME="`pwd -P`" cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS="" # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" warn () { echo "$*" } die () { echo echo "$*" echo exit 1 } # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false case "`uname`" in CYGWIN* ) cygwin=true ;; Darwin* ) darwin=true ;; MINGW* ) msys=true ;; NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD="java" which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi # Increase the maximum file descriptors if we can. if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then MAX_FD="$MAX_FD_LIMIT" fi ulimit -n $MAX_FD if [ $? -ne 0 ] ; then warn "Could not set maximum file descriptor limit: $MAX_FD" fi else warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" fi fi # For Darwin, add options to specify how the application appears in the dock if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi # For Cygwin, switch paths to Windows format before running java if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` SEP="" for dir in $ROOTDIRSRAW ; do ROOTDIRS="$ROOTDIRS$SEP$dir" SEP="|" done OURCYGPATTERN="(^($ROOTDIRS))" # Add a user-defined pattern to the cygpath arguments if [ "$GRADLE_CYGPATTERN" != "" ] ; then OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" fi # Now convert the arguments - kludge to limit ourselves to /bin/sh i=0 for arg in "$@" ; do CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` else eval `echo args$i`="\"$arg\"" fi i=$((i+1)) done case $i in (0) set -- ;; (1) set -- "$args0" ;; (2) set -- "$args0" "$args1" ;; (3) set -- "$args0" "$args1" "$args2" ;; (4) set -- "$args0" "$args1" "$args2" "$args3" ;; (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi # Escape application args save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } APP_ARGS=$(save "$@") # Collect all arguments for the java command, following the shell quoting and substitution rules eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then cd "$(dirname "$0")" fi exec "$JAVACMD" "$@" ================================================ FILE: gradlew.bat ================================================ @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS= @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if "%ERRORLEVEL%" == "0" goto init echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto init echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :init @rem Get command-line arguments, handling Windows variants if not "%OS%" == "Windows_NT" goto win9xME_args :win9xME_args @rem Slurp the command line arguments. set CMD_LINE_ARGS= set _SKIP=2 :win9xME_args_slurp if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% :end @rem End local scope for the variables with windows NT shell if "%ERRORLEVEL%"=="0" goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 exit /b 1 :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: release.gradle ================================================ apply plugin: 'maven' apply plugin: 'signing' def isReleaseBuild() { return VERSION_NAME.contains("SNAPSHOT") == false } def getMavenRepositoryUrl() { return hasProperty('repositoryUrl') ? property('repositoryUrl') : "https://oss.sonatype.org/service/local/staging/deploy/maven2/" } def getMavenRepositoryUsername() { return hasProperty('repositoryUsername') ? property('repositoryUsername') : "" } def getMavenRepositoryPassword() { return hasProperty('repositoryPassword') ? property('repositoryPassword') : "" } def configureStethoPom(def pom) { pom.whenConfigured { applyOptionalDeps it, getOptionalDeps() } pom.project { name POM_NAME artifactId POM_ARTIFACT_ID packaging POM_PACKAGING description 'Stetho Debugging Platform for Android' url 'https://github.com/facebook/stetho' scm { url 'https://github.com/facebook/stetho.git' connection 'scm:git:https://github.com/facebook/stetho.git' developerConnection 'scm:git:git@github.com:facebook/stetho.git' } licenses { license { name 'MIT License' url 'https://github.com/facebook/stetho/blob/master/LICENSE' distribution 'repo' } } developers { developer { id 'facebook' name 'Facebook' } } } } // Hack to modify the resulting pom's dependencies to use // true where appropriate. def applyOptionalDeps(def pom, def optionalDeps) { pom.dependencies.each { dep -> def artifactLabel = dep.groupId + ':' + dep.artifactId if (optionalDeps.contains(artifactLabel)) { dep.optional = true } } } def getOptionalDeps() { if (hasProperty('POM_OPTIONAL_DEPS')) { return property('POM_OPTIONAL_DEPS').split(',') as Set } else { return [] } } afterEvaluate { project -> task androidJavadoc(type: Javadoc) { source = android.sourceSets.main.java.srcDirs classpath += files(android.bootClasspath) if (JavaVersion.current().isJava8Compatible()) { options.addStringOption('Xdoclint:none', '-quiet') } } task androidJavadocJar(type: Jar, dependsOn: androidJavadoc) { classifier = 'javadoc' from androidJavadoc.destinationDir } task androidSourcesJar(type: Jar) { classifier = 'sources' from android.sourceSets.main.java.srcDirs } android.libraryVariants.all { variant -> def name = variant.name.capitalize() task "jar${name}"(type: Jar, dependsOn: variant.javaCompile) { from variant.javaCompile.destinationDir } } artifacts { archives androidJavadocJar archives androidSourcesJar } version = VERSION_NAME group = GROUP signing { required { isReleaseBuild() && gradle.taskGraph.hasTask("uploadArchives") } sign configurations.archives } uploadArchives { configuration = configurations.archives repositories.mavenDeployer { beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } repository(url: getMavenRepositoryUrl()) { authentication( userName: getMavenRepositoryUsername(), password: getMavenRepositoryPassword()) } configureStethoPom pom } } task installArchives(type: Upload) { configuration = configurations.archives repositories { mavenDeployer { repository url: "file://${System.properties['user.home']}/.m2/repository" configureStethoPom pom } } } } ================================================ FILE: scripts/.gitignore ================================================ __pycache__ ================================================ FILE: scripts/dumpapp ================================================ #!/usr/bin/env python3 import sys import os import io from stetho_open import * def main(): # Manually parse out -p , all other option handling occurs inside # the hosting process. # Connect to the process passed in via -p. If that is not supplied fallback # the process defined in STETHO_PROCESS. If neither are defined throw. process = os.environ.get('STETHO_PROCESS') args = sys.argv[1:] if len(args) > 0 and (args[0] == '-p' or args[0] == '--process'): if len(args) < 2: sys.exit('Missing ') else: process = args[1] args = args[2:] # Connect to ANDROID_SERIAL if supplied, otherwise fallback to any # transport. device = os.environ.get('ANDROID_SERIAL') # Connect on the overridden port if specified port = get_adb_server_port() try: sock = stetho_open(device, process, port) # Send dumpapp hello (DUMP + version=1) sock.send(b'DUMP' + struct.pack('!l', 1)) enter_frame = b'!' + struct.pack('!l', len(args)) for arg in args: argAsUTF8 = arg.encode('utf-8') enter_frame += struct.pack( '!H' + str(len(argAsUTF8)) + 's', len(argAsUTF8), argAsUTF8) sock.send(enter_frame) read_frames(sock) except HumanReadableError as e: sys.exit(e) except BrokenPipeError as e: sys.exit(0) except KeyboardInterrupt: sys.exit(1) def read_frames(sock): while True: # All frames have a single character code followed by a big-endian int code = read_input(sock, 1, 'code') n = struct.unpack('!l', read_input(sock, 4, 'int4'))[0] if code == b'1': if n > 0: sys.stdout.buffer.write(read_input(sock, n, 'stdout blob')) sys.stdout.buffer.flush() elif code == b'2': if n > 0: sys.stderr.buffer.write(read_input(sock, n, 'stderr blob')) sys.stderr.buffer.flush() elif code == b'_': if n > 0: data = sys.stdin.buffer.read(n) if len(data) == 0: sock.send(b'-' + struct.pack('!l', -1)) else: sock.send(b'-' + struct.pack('!l', len(data)) + data) elif code == b'x': sys.exit(n) else: if raise_on_eof: raise IOError('Unexpected header: %s' % code) break if __name__ == '__main__': main() ================================================ FILE: scripts/hprof_dump.sh ================================================ #!/bin/bash DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" DUMPAPP="$DIR/dumpapp" set -e # This will generate an hprof on the device, download it locally, convert the # hprof to the standard format, and store it in the current working directory. # The resulting file can be explored with a tool such as the standalone Eclipse # MemoryAnalyzer: https://eclipse.org/mat/ if [[ -z "$1" ]]; then OUTFILE="out.hprof" else OUTFILE=$1 fi TEMPFILE="${OUTFILE}-dalvik.tmp" echo "Generating hprof on device (this can take a while)..." $DUMPAPP "$@" hprof - > ${TEMPFILE} echo "Converting $TEMPFILE to standard format..." hprof-conv $TEMPFILE $OUTFILE rm $TEMPFILE echo "Stored ${OUTFILE}" ================================================ FILE: scripts/stetho_open.py ================================================ #!/usr/bin/env python3 ############################################################################### ## ## Simple utility class to create a forwarded socket connection to an ## application's stetho domain socket. ## ## Usage: ## ## sock = stetho_open( ## device='', ## process='com.facebook.stetho.sample') ## doHttp(sock) ## ############################################################################### import os import socket import struct import re def get_adb_server_port_from_server_socket(): socket_spec = os.environ.get('ADB_SERVER_SOCKET') if not socket_spec: return None if not socket_spec.startswith('tcp:'): raise HumanReadableError( 'Invalid or unsupported socket spec \'%s\' specified in ADB_SERVER_SOCKET.' % ( socket_spec)) return socket_spec.split(':')[-1] def get_adb_server_port(): defaultPort = 5037 portStr = get_adb_server_port_from_server_socket() or os.environ.get('ANDROID_ADB_SERVER_PORT') if portStr is None: return defaultPort elif portStr.isdigit(): return int(portStr) else: raise HumanReadableError( 'Invalid integer \'%s\' specified in ANDROID_ADB_SERVER_PORT or ADB_SERVER_SOCKET.' % ( portStr)) def stetho_open(device=None, process=None, port=None): if port is None: port = get_adb_server_port() adb = _connect_to_device(device, port) socket_name = None if process is None: socket_name = _find_only_stetho_socket(device, port) else: socket_name = _format_process_as_stetho_socket(process) try: adb.select_service('localabstract:%s' % (socket_name)) except SelectServiceError as e: raise HumanReadableError( 'Failure to target process %s: %s (is it running?)' % ( process, e.reason)) return adb.sock def read_input(sock, n, tag): data = b''; while len(data) < n: incoming_data = sock.recv(n - len(data)) if len(incoming_data) == 0: break data += incoming_data if len(data) != n: raise IOError('Unexpected end of stream while reading %s.' % tag) return data def _find_only_stetho_socket(device, port): adb = _connect_to_device(device, port) try: adb.select_service('shell:cat /proc/net/unix') last_stetho_socket_name = None process_names = [] for line in adb.sock.makefile(): row = re.split(r'\s+', line.rstrip()) if len(row) < 8: continue socket_name = row[7] if not socket_name.startswith('@stetho_'): continue # Filter out entries that are not server sockets if int(row[3], 16) != 0x10000 or int(row[5]) != 1: continue last_stetho_socket_name = socket_name[1:] process_names.append( _parse_process_from_stetho_socket(socket_name)) if len(process_names) > 1: raise HumanReadableError( 'Multiple stetho-enabled processes available:%s\n' % ( '\n\t'.join([''] + list(set(process_names)))) + 'Use -p or the environment variable STETHO_PROCESS to ' + 'select one') elif last_stetho_socket_name == None: raise HumanReadableError('No stetho-enabled processes running') else: return last_stetho_socket_name finally: adb.sock.close() def _connect_to_device(device=None, port=None): if port is None: raise HumanReadableError('Must specify a port when calling _connect_to_device') adb = AdbSmartSocketClient() adb.connect(port) try: if device is None: adb.select_service('host:transport-any') else: adb.select_service('host:transport:%s' % (device)) return adb except SelectServiceError as e: raise HumanReadableError( 'Failure to target device %s: %s' % (device, e.reason)) def _parse_process_from_stetho_socket(socket_name): m = re.match("^\@stetho_(.+)_devtools_remote$", socket_name) if m is None: raise Exception('Unexpected Stetho socket formatting: %s' % (socket_name)) return m.group(1) def _format_process_as_stetho_socket(process): return 'stetho_%s_devtools_remote' % (process) class AdbSmartSocketClient(object): """Implements the smartsockets system defined by: https://android.googlesource.com/platform/system/core/+/master/adb/protocol.txt """ def __init__(self): pass def connect(self, port=5037): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: sock.connect(('localhost', port)) except ConnectionRefusedError: sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) sock.connect(('localhost', port)) self.sock = sock def select_service(self, service): message = '%04x%s' % (len(service), service) self.sock.send(message.encode('ascii')) status = read_input(self.sock, 4, "status") if status == b'OKAY': # All good... pass elif status == b'FAIL': reason_len = int(read_input(self.sock, 4, "fail reason"), 16) reason = read_input(self.sock, reason_len, "fail reason lean").decode('ascii') raise SelectServiceError(reason) else: raise Exception('Unrecognized status=%s' % (status)) class SelectServiceError(Exception): def __init__(self, reason): self.reason = reason def __str__(self): return repr(self.reason) class HumanReadableError(Exception): def __init__(self, reason): self.reason = reason def __str__(self): return self.reason ================================================ FILE: settings.gradle ================================================ include ':stetho' include ':stetho-urlconnection' include ':stetho-okhttp' include ':stetho-okhttp3' include ':stetho-js-rhino' include ':stetho-sample' include ':stetho-timber' ================================================ FILE: stetho/.gitignore ================================================ /build ================================================ FILE: stetho/build.gradle ================================================ apply plugin: 'com.android.library' android { compileSdkVersion rootProject.ext.compileSdkVersion defaultConfig { minSdkVersion 14 targetSdkVersion rootProject.ext.targetSdkVersion versionCode 1 versionName "1.0" consumerProguardFiles 'proguard-consumer.pro' testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } } dependencies { implementation 'commons-cli:commons-cli:1.2' implementation 'com.google.code.findbugs:jsr305:3.0.2' implementation 'androidx.annotation:annotation:1.1.0' // Optional: reflection is used to test whether Fragment (and the transient AndroidX Core) are actually present. implementation 'androidx.appcompat:appcompat:1.2.0' // optional testImplementation 'junit:junit:4.12' testImplementation('org.robolectric:robolectric:2.4') { exclude module: 'commons-logging' exclude module: 'httpclient' } testImplementation 'org.powermock:powermock-api-mockito:1.6.6' testImplementation 'org.powermock:powermock-module-junit4:1.6.6' } apply from: rootProject.file('release.gradle') android.libraryVariants.all { variant -> def name = variant.name.capitalize() // Ugly kludge to rename license files in the bundled commons-cli // dependency so that they do not appear to describe Stetho's license. task "tidyCommonsCli${name}"(type: Copy) { from { variant.javaCompile.classpath.findAll { it.getName() == 'commons-cli-1.2.jar' }.collect { zipTree(it) } } into "build/commons-cli-tidy-${name}" rename 'LICENSE', 'commons-cli-LICENSE' rename 'NOTICE', 'commons-cli-NOTICE' } task "metainf${name}"(type: Copy) { from rootProject.file('LICENSE') into "build/metainf-${name}/META-INF" } task "fatjar${name}"(type: Jar, dependsOn: [ "jar${name}", "tidyCommonsCli${name}", "metainf${name}" ]) { classifier = 'fatjar' from variant.javaCompile.destinationDir from "build/commons-cli-tidy-${name}" from "build/metainf-${name}" exclude "android/support/**/*" } } ================================================ FILE: stetho/gradle.properties ================================================ POM_NAME=Stetho POM_ARTIFACT_ID=stetho POM_PACKAGING=aar ================================================ FILE: stetho/proguard-consumer.pro ================================================ -keep class com.facebook.stetho.** { *; } -dontwarn com.facebook.stetho.** ================================================ FILE: stetho/src/main/AndroidManifest.xml ================================================ ================================================ FILE: stetho/src/main/java/com/facebook/stetho/DumperPluginsProvider.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho; import com.facebook.stetho.dumpapp.DumperPlugin; /** * Provider interface to lazily supply dumpers to be initialized on demand. It is critical * that the main initialization flow of Stetho perform no actual work beyond simply * binding a socket and starting the listener thread. */ public interface DumperPluginsProvider { Iterable get(); } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/InspectorModulesProvider.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho; import com.facebook.stetho.inspector.protocol.ChromeDevtoolsDomain; public interface InspectorModulesProvider { Iterable get(); } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/Stetho.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ 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.ProtocolDetectingSocketHandler; import com.facebook.stetho.server.ServerManager; import com.facebook.stetho.server.SocketHandler; import com.facebook.stetho.server.SocketHandlerFactory; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import javax.annotation.Nullable; /** * Initialization and configuration entry point for the Stetho debugging system. Simple usage with * default plugins and features enabled: *

*

 *   Stetho.initializeWithDefaults(context)
 * 
*

* 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. *

* 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 getDumperPlugins() { return new DefaultDumperPluginsBuilder(context).finish(); } @Override protected Iterable 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 get() { return new DefaultDumperPluginsBuilder(context).finish(); } }; } public static InspectorModulesProvider defaultInspectorModulesProvider(final Context context) { return new InspectorModulesProvider() { @Override public Iterable get() { return new DefaultInspectorModulesBuilder(context).finish(); } }; } private static class PluginBuilder { private final Set mProvidedNames = new HashSet<>(); private final Set mRemovedNames = new HashSet<>(); private final ArrayList 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.add(pluginName); } private void throwIfFinished() { if (mFinished) { throw new IllegalStateException("Must not continue to build after finish()"); } } public Iterable 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 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 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 mDelegate = new PluginBuilder<>(); @Nullable private DocumentProviderFactory mDocumentProvider; @Nullable private RuntimeReplFactory mRuntimeRepl; @Nullable private DatabaseFilesProvider mDatabaseFilesProvider; @Nullable private List 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. *

* Experimental. 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. *

* 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. * *

This method is deprecated and instead it is recommended that you explicitly * configure the {@link SqliteDatabaseDriver} as with:

*
     *   provideDatabaseDriver(
     *     new SqliteDatabaseDriver(
     *       context,
     *       new MyDatabaseFilesProvider(...),
     *       new DefaultDatabaseConnectionProvider(...)))
     * 
* * @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: *
    *
  1. {@link SqliteDatabaseDriver} - Presents SQLite databases.
  2. *
  3. {@link ContentProviderDatabaseDriver} - Configure and present content provider * data.
  4. *
* *

Stetho assumes the {@link SqliteDatabaseDriver} should be installed if * no driver of that type is provided and {@link #excludeSqliteDatabaseDriver} is not * used.

*/ 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 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.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 getDumperPlugins(); @Nullable protected abstract Iterable 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 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 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. *

* 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 getDumperPlugins() { return mDumperPlugins != null ? mDumperPlugins.get() : null; } @Nullable @Override protected Iterable getInspectorModules() { return mInspectorModules != null ? mInspectorModules.get() : null; } } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/common/Accumulator.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.common; public interface Accumulator { void store(E object); } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/common/ArrayListAccumulator.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.common; import java.util.ArrayList; public final class ArrayListAccumulator extends ArrayList implements Accumulator { @Override public void store(E object) { add(object); } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/common/ExceptionUtil.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.common; public class ExceptionUtil { @SuppressWarnings("unchecked") public static void propagateIfInstanceOf(Throwable t, Class type) throws T { if (type.isInstance(t)) { throw (T)t; } } public static RuntimeException propagate(Throwable t) { propagateIfInstanceOf(t, Error.class); propagateIfInstanceOf(t, RuntimeException.class); throw new RuntimeException(t); } @SuppressWarnings("unchecked") public static void sneakyThrow(Throwable t) throws T { throw (T)t; } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/common/ListUtil.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.common; import java.util.AbstractList; import java.util.Collections; import java.util.List; import java.util.RandomAccess; public final class ListUtil { private ListUtil() { } /** * Compares the contents of two {@link List}s by using object identity. */ public static boolean identityEquals(List list1, List list2) { if (list1 == list2) { return true; } int size = list1.size(); if (size != list2.size()) { return false; } for (int i = 0; i < size; ++i) { if (list1.get(i) != list2.get(i)) { return false; } } return true; } /** * Copies the given {@link List} and returns the copy as an immutable {@link List}. */ public static List copyToImmutableList(List list) { if (list instanceof ImmutableList) { return list; } int size = list.size(); switch (size) { case 0: return Collections.emptyList(); case 1: return new OneItemImmutableList<>(list.get(0)); case 2: return new TwoItemImmutableList<>(list.get(0), list.get(1)); case 3: return new ThreeItemImmutableList<>(list.get(0), list.get(1), list.get(2)); case 4: return new FourItemImmutableList<>(list.get(0), list.get(1), list.get(2), list.get(3)); case 5: return new FiveItemImmutableList<>( list.get(0), list.get(1), list.get(2), list.get(3), list.get(4)); default: Object[] array = list.toArray(); return new ImmutableArrayList<>(array); } } public static List newImmutableList(T item) { return new OneItemImmutableList<>(item); } public static List newImmutableList(T itemOne, T itemTwo) { return new TwoItemImmutableList<>(itemOne, itemTwo); } private static interface ImmutableList extends List, RandomAccess { } private static final class ImmutableArrayList extends AbstractList implements ImmutableList { private final Object[] mArray; public ImmutableArrayList(Object[] array) { mArray = array; } @Override @SuppressWarnings("unchecked") public E get(int location) { return (E) mArray[location]; } @Override public int size() { return mArray.length; } } private static final class OneItemImmutableList extends AbstractList implements ImmutableList { private final E mItem; public OneItemImmutableList(E item) { mItem = item; } @Override public E get(int location) { if (location == 0) { return mItem; } else { throw new IndexOutOfBoundsException(); } } @Override public int size() { return 1; } } private static final class TwoItemImmutableList extends AbstractList implements ImmutableList { private final E mItem0; private final E mItem1; public TwoItemImmutableList(E item0, E item1) { mItem0 = item0; mItem1 = item1; } @Override public E get(int location) { switch (location) { case 0: return mItem0; case 1: return mItem1; default: throw new IndexOutOfBoundsException(); } } @Override public int size() { return 2; } } private static final class ThreeItemImmutableList extends AbstractList implements ImmutableList { private final E mItem0; private final E mItem1; private final E mItem2; public ThreeItemImmutableList(E item0, E item1, E item2) { mItem0 = item0; mItem1 = item1; mItem2 = item2; } @Override public E get(int location) { switch (location) { case 0: return mItem0; case 1: return mItem1; case 2: return mItem2; default: throw new IndexOutOfBoundsException(); } } @Override public int size() { return 3; } } private static final class FourItemImmutableList extends AbstractList implements ImmutableList { private final E mItem0; private final E mItem1; private final E mItem2; private final E mItem3; public FourItemImmutableList(E item0, E item1, E item2, E item3) { mItem0 = item0; mItem1 = item1; mItem2 = item2; mItem3 = item3; } @Override public E get(int location) { switch (location) { case 0: return mItem0; case 1: return mItem1; case 2: return mItem2; case 3: return mItem3; default: throw new IndexOutOfBoundsException(); } } @Override public int size() { return 4; } } private static final class FiveItemImmutableList extends AbstractList implements ImmutableList { private final E mItem0; private final E mItem1; private final E mItem2; private final E mItem3; private final E mItem4; public FiveItemImmutableList(E item0, E item1, E item2, E item3, E item4) { mItem0 = item0; mItem1 = item1; mItem2 = item2; mItem3 = item3; mItem4 = item4; } @Override public E get(int location) { switch (location) { case 0: return mItem0; case 1: return mItem1; case 2: return mItem2; case 3: return mItem3; case 4: return mItem4; default: throw new IndexOutOfBoundsException(); } } @Override public int size() { return 5; } } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/common/LogRedirector.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.common; import java.io.PrintWriter; import java.io.StringWriter; import android.util.Log; /** * Logging interface which allows third party code to replace Android's {@link Log} API with * their own implementation. Useful in public libraries so that callers can more finely control * how log messages are produced. *

* Note that the API and implementation of this class are designed to match the semantics of * the default system {@link Log} API. */ public class LogRedirector { private static volatile Logger sLogger; /** * Override Android's default {@link Log} API with a custom logger interface. This affects * all subsequent calls to {@link LogRedirector} APIs. */ public static void setLogger(Logger logger) { Util.throwIfNull(logger); Util.throwIfNotNull(sLogger); sLogger = logger; } public static void e(String tag, String message, Throwable t) { e(tag, message + "\n" + formatThrowable(t)); } public static void e(String tag, String message) { log(Log.ERROR, tag, message); } public static void w(String tag, String message, Throwable t) { w(tag, message + "\n" + formatThrowable(t)); } public static void w(String tag, String message) { log(Log.WARN, tag, message); } public static void i(String tag, String message, Throwable t) { i(tag, message + "\n" + formatThrowable(t)); } public static void i(String tag, String message) { log(Log.INFO, tag, message); } public static void d(String tag, String message, Throwable t) { d(tag, message + "\n" + formatThrowable(t)); } public static void d(String tag, String message) { log(Log.DEBUG, tag, message); } public static void v(String tag, String message, Throwable t) { v(tag, message + "\n" + formatThrowable(t)); } public static void v(String tag, String message) { log(Log.VERBOSE, tag, message); } private static String formatThrowable(Throwable t) { StringWriter buf = new StringWriter(); PrintWriter writer = new PrintWriter(buf); t.printStackTrace(); writer.flush(); return buf.toString(); } private static void log(int priority, String tag, String message) { Logger logger = sLogger; if (logger != null) { logger.log(priority, tag, message); } else { Log.println(priority, tag, message); } } public static boolean isLoggable(String tag, int priority) { Logger logger = sLogger; if (logger != null) { return logger.isLoggable(tag, priority); } else { return Log.isLoggable(tag, priority); } } /** * Custom logger implementation that can forward logs to something other than Android's * {@link Log} API. */ public interface Logger { /** * Allows the caller to query if a particular tag and priority will be logged. Note that just * like with {@link Log#isLoggable}, callers are required to consult this manually if the * result is to be considered. */ boolean isLoggable(String tag, int priority); /** * Submit a log message. */ void log(int priority, String tag, String message); } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/common/LogUtil.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.common; import java.util.Locale; import android.util.Log; /** * Logging helper specifically for use by Stetho internals. */ public class LogUtil { private static final String TAG = "stetho"; public static void e(String format, Object... args) { e(format(format, args)); } public static void e(Throwable t, String format, Object... args) { e(t, format(format, args)); } public static void e(String message) { if (isLoggable(Log.ERROR)) { LogRedirector.e(TAG, message); } } public static void e(Throwable t, String message) { if (isLoggable(Log.ERROR)) { LogRedirector.e(TAG, message, t); } } public static void w(String format, Object... args) { w(format(format, args)); } public static void w(Throwable t, String format, Object... args) { w(t, format(format, args)); } public static void w(String message) { if (isLoggable(Log.WARN)) { LogRedirector.w(TAG, message); } } public static void w(Throwable t, String message) { if (isLoggable(Log.WARN)) { LogRedirector.w(TAG, message, t); } } public static void i(String format, Object... args) { i(format(format, args)); } public static void i(Throwable t, String format, Object... args) { i(t, format(format, args)); } public static void i(String message) { if (isLoggable(Log.INFO)) { LogRedirector.i(TAG, message); } } public static void i(Throwable t, String message) { if (isLoggable(Log.INFO)) { LogRedirector.i(TAG, message, t); } } public static void d(String format, Object... args) { d(format(format, args)); } public static void d(Throwable t, String format, Object... args) { d(t, format(format, args)); } public static void d(String message) { if (isLoggable(Log.DEBUG)) { LogRedirector.d(TAG, message); } } public static void d(Throwable t, String message) { if (isLoggable(Log.DEBUG)) { LogRedirector.d(TAG, message, t); } } public static void v(String format, Object... args) { v(format(format, args)); } public static void v(Throwable t, String format, Object... args) { v(t, format(format, args)); } public static void v(String message) { if (isLoggable(Log.VERBOSE)) { LogRedirector.v(TAG, message); } } public static void v(Throwable t, String message) { if (isLoggable(Log.VERBOSE)) { LogRedirector.v(TAG, message, t); } } private static String format(String format, Object... args) { return String.format(Locale.US, format, args); } /** * Applies an internal policy on whether to use {@link LogRedirector#isLoggable(String, int)}. * This interface is sometimes sidestepped to avoid Android's default fairly awkward * {@link Log#isLoggable(String, int)} interface from blocking important messages while still * offering users of Stetho to suppress our error/warning logs via * {@link LogRedirector#setLogger(LogRedirector.Logger)}. */ public static boolean isLoggable(int priority) { switch (priority) { case Log.ERROR: case Log.WARN: return true; default: return LogRedirector.isLoggable(TAG, priority); } } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/common/Predicate.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.common; public interface Predicate { boolean apply(T t); } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/common/ProcessUtil.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.common; import android.os.Build; import android.os.Process; import android.os.UserManager; import androidx.annotation.Keep; import androidx.annotation.RequiresApi; import javax.annotation.Nullable; import java.io.FileInputStream; import java.io.IOException; public class ProcessUtil { /** * Maximum length allowed in {@code /proc/self/cmdline}. Imposed to avoid a large buffer * allocation during the init path. */ private static final int CMDLINE_BUFFER_SIZE = 64; private static String sProcessName; private static boolean sProcessNameRead; /** * Get process name by reading {@code /proc/self/cmdline}. * * @return Process name or null if there was an error reading from {@code /proc/self/cmdline}. * It is unknown how this error can occur in practice and should be considered extremely * rare. */ @Nullable public static synchronized String getProcessName() { if (!sProcessNameRead) { sProcessNameRead = true; try { sProcessName = readProcessName(); } catch (IOException e) { } } return sProcessName; } private static String readProcessName() throws IOException { byte[] cmdlineBuffer = new byte[CMDLINE_BUFFER_SIZE]; // Avoid using a Reader to not pick up a forced 16K buffer. Silly java.io... FileInputStream stream = new FileInputStream("/proc/self/cmdline"); boolean success = false; try { int n = stream.read(cmdlineBuffer); success = true; int endIndex = indexOf(cmdlineBuffer, 0, n, (byte)0 /* needle */); return new String(cmdlineBuffer, 0, endIndex > 0 ? endIndex : n); } finally { Util.close(stream, !success); } } private static int indexOf(byte[] haystack, int offset, int length, byte needle) { for (int i = 0; i < haystack.length; i++) { if (haystack[i] == needle) { return i; } } return -1; } public static int getUserId() { // On multi-user devices, user id is calculated from process uid. // On single-user devices, user id is always 0. // https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/UserHandle.java;l=282;drc=5a5038ddb87b9c4ac576935b77cab4688169ee48 final boolean supportsMultipleUsers = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && UserManager21Impl.supportsMultipleUsers(); return supportsMultipleUsers ? Process.myUid() / 100000 : 0; } @Keep @RequiresApi(Build.VERSION_CODES.N) private static class UserManager21Impl { public static boolean supportsMultipleUsers() { return UserManager.supportsMultipleUsers(); } } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/common/ReflectionUtil.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.common; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import javax.annotation.Nullable; public final class ReflectionUtil { private ReflectionUtil() { } @Nullable public static Class tryGetClassForName(String className) { try { return Class.forName(className); } catch (ClassNotFoundException e) { return null; } } @Nullable public static Field tryGetDeclaredField(Class theClass, String fieldName) { try { return theClass.getDeclaredField(fieldName); } catch (NoSuchFieldException e) { LogUtil.d( e, "Could not retrieve %s field from %s", fieldName, theClass); return null; } } @Nullable public static Object getFieldValue(Field field, Object target) { try { return field.get(target); } catch (IllegalAccessException e) { throw new RuntimeException(e); } } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/common/StringUtil.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.common; public final class StringUtil { private StringUtil() { } @SuppressWarnings("StringEquality") public static String removePrefix(String string, String prefix, String previousAttempt) { if (string != previousAttempt) { return previousAttempt; } else { return removePrefix(string, prefix); } } public static String removePrefix(String string, String prefix) { if (string.startsWith(prefix)) { return string.substring(prefix.length()); } else { return string; } } public static String removeAll(String string, char target) { final int length = string.length(); final StringBuilder builder = new StringBuilder(length); for (int i = 0; i < length; ++i) { char c = string.charAt(i); if (c != target) { builder.append(c); } } return builder.toString(); } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/common/ThreadBound.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.common; import java.lang.IllegalStateException; /** * Implemented by an object whose methods must be called on a specific thread. If a method is * called from a disallowed thread then {@link IllegalStateException} will be thrown. * To marshal a call to the correct thread, you can use {@link #postAndWait(UncheckedCallable)} or * {@link #postAndWait(Runnable)}, both of which complete synchronously. */ public interface ThreadBound { /** * Checks whether the current thread has access to this object. * @return true if this thread has access to this object; otherwise false */ boolean checkThreadAccess(); /** * Enforces that the current thread has access to this object. * @throws IllegalStateException if the current thread does not have access to this object */ void verifyThreadAccess(); /** * Synchronously executes an {@link UncheckedCallable} on the thread that this object is bound to, * and returns its result. * @param c the {@link UncheckedCallable} to execute * @param the return type of the {@link UncheckedCallable} * @return the return value from {@link UncheckedCallable#call()} * @throws RuntimeException if the {@link UncheckedCallable} could not be executed (the cause * will be null), or if {@link UncheckedCallable#call()} threw an exception (the cause will be the * exception that it threw). */ V postAndWait(UncheckedCallable c); /** * Synchronously executes a {@link Runnable} on the thread that this object is bound to. * @param r the {@link Runnable} to execute * @throws RuntimeException if the {@link Runnable} could not be executed (the cause will be * null), or if {@link Runnable#run()} threw an exception (the cause will be the exception that * it threw). */ void postAndWait(Runnable r); /** * Asynchronously executes a {@link Runnable} on the thread that this object is bound to * after the specified delay. * @param r the {@link Runnable} to execute * @param delayMillis The delay (in milliseconds) until the {@link Runnable} will be executed. * @throws RuntimeException if the {@link Runnable} could not be enqueued. */ void postDelayed(Runnable r, long delayMillis); /** * Removes any pending posts of the given {@link Runnable} that are in the queue. * @param r the {@link Runnable} to remove from the queue */ void removeCallbacks(Runnable r); } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/common/UncheckedCallable.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.common; /** * A task that returns a result. Implementers define a single method with no arguments called * {@code call}. * *

This interface is identical to {@link java.util.concurrent.Callable} but without the checked * exception. * * @param the result type of method {@code call} */ public interface UncheckedCallable { V call(); } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/common/Utf8Charset.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.common; import java.io.UnsupportedEncodingException; import java.nio.charset.Charset; public class Utf8Charset { public static final String NAME = "UTF-8"; public static final Charset INSTANCE = Charset.forName(NAME); public static byte[] encodeUTF8(String str) { try { return str.getBytes(NAME); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } } public static String decodeUTF8(byte[] bytes) { return new String(bytes, INSTANCE); } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/common/Util.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.common; import java.io.ByteArrayOutputStream; import java.io.Closeable; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; public class Util { public static T throwIfNull(T item) { if (item == null) { throw new NullPointerException(); } return item; } public static void throwIfNull(T1 item1, T2 item2) { throwIfNull(item1); throwIfNull(item2); } public static void throwIfNull(T1 item1, T2 item2, T3 item3) { throwIfNull(item1); throwIfNull(item2); throwIfNull(item3); } public static void throwIfNotNull(Object item) { if (item != null) { throw new IllegalStateException(); } } public static void throwIf(boolean condition) { if (condition) { throw new IllegalStateException(); } } public static void throwIfNot(boolean condition) { if (!condition) { throw new IllegalStateException(); } } public static void throwIfNot(boolean condition, String format, Object...args) { if (!condition) { String message = String.format(format, args); throw new IllegalStateException(message); } } public static void copy(InputStream input, OutputStream output, byte[] buffer) throws IOException { int n; while ((n = input.read(buffer)) != -1) { output.write(buffer, 0, n); } } public static void close(Closeable closeable, boolean hideException) throws IOException { if (closeable != null) { if (hideException) { try { closeable.close(); } catch (IOException e) { LogUtil.e(e, "Hiding IOException because another is pending"); } } else { closeable.close(); } } } public static void sleepUninterruptibly(long millis) { long remaining = millis; long startTime = System.currentTimeMillis(); do { try { Thread.sleep(remaining); return; } catch (InterruptedException e) { long sleptFor = System.currentTimeMillis() - startTime; remaining -= sleptFor; } } while (remaining > 0); } public static void joinUninterruptibly(Thread t) { while (true) { try { t.join(); return; } catch (InterruptedException e) { // Keep going... } } } public static void awaitUninterruptibly(CountDownLatch latch) { while (true) { try { latch.await(); return; } catch (InterruptedException e) { // Keep going... } } } public static T getUninterruptibly( Future future, long timeout, TimeUnit unit) throws TimeoutException, ExecutionException { long remaining = unit.toMillis(timeout); long startTime = System.currentTimeMillis(); while (true) { try { return future.get(remaining, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { long gotFor = System.currentTimeMillis() - startTime; remaining -= gotFor; } } } public static T getUninterruptibly(Future future) throws ExecutionException { while (true) { try { return future.get(); } catch (InterruptedException e) { //Keep going... } } } public static String readAsUTF8(InputStream in) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); copy(in, out, new byte[1024]); return out.toString("UTF-8"); } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/common/android/AccessibilityUtil.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.common.android; import android.text.TextUtils; import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; import android.widget.AdapterView; import android.widget.HorizontalScrollView; import android.widget.ScrollView; import android.widget.Spinner; import java.util.List; import androidx.annotation.Nullable; import androidx.core.view.ViewCompat; import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; /** * This class provides utility methods for determining certain accessibility properties of * {@link View}s and {@link AccessibilityNodeInfoCompat}s. It is porting some of the checks from * {@link com.googlecode.eyesfree.utils.AccessibilityNodeInfoUtils}, but has stripped many features * which are unnecessary here. */ public final class AccessibilityUtil { private AccessibilityUtil() { } /** * Returns whether the specified node has text or a content description. * * @param node The node to check. * @return {@code true} if the node has text. */ public static boolean hasText(@Nullable AccessibilityNodeInfoCompat node) { if (node == null) { return false; } return !TextUtils.isEmpty(node.getText()) || !TextUtils.isEmpty(node.getContentDescription()); } /** * Returns whether the supplied {@link View} and {@link AccessibilityNodeInfoCompat} would * produce spoken feedback if it were accessibility focused. NOTE: not all speaking nodes are * focusable. * * @param view The {@link View} to evaluate * @param node The {@link AccessibilityNodeInfoCompat} to evaluate * @return {@code true} if it meets the criterion for producing spoken feedback */ public static boolean isSpeakingNode( @Nullable AccessibilityNodeInfoCompat node, @Nullable View view) { if (node == null || view == null) { return false; } if (!node.isVisibleToUser()) { return false; } int important = ViewCompat.getImportantForAccessibility(view); if (important == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS || (important == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO && node.getChildCount() <= 0)) { return false; } return node.isCheckable() || hasText(node) || hasNonActionableSpeakingDescendants(node, view); } /** * Determines if the supplied {@link View} and {@link AccessibilityNodeInfoCompat} has any * children which are not independently accessibility focusable and also have a spoken * description. *

* NOTE: Accessibility services will include these children's descriptions in the closest * focusable ancestor. * * @param view The {@link View} to evaluate * @param node The {@link AccessibilityNodeInfoCompat} to evaluate * @return {@code true} if it has any non-actionable speaking descendants within its subtree */ public static boolean hasNonActionableSpeakingDescendants( @Nullable AccessibilityNodeInfoCompat node, @Nullable View view) { if (node == null || view == null || !(view instanceof ViewGroup)) { return false; } ViewGroup viewGroup = (ViewGroup) view; for (int i = 0, count = viewGroup.getChildCount(); i < count; i++) { View childView = viewGroup.getChildAt(i); if (childView == null) { continue; } AccessibilityNodeInfoCompat childNode = AccessibilityNodeInfoCompat.obtain(); try { ViewCompat.onInitializeAccessibilityNodeInfo(childView, childNode); if (isAccessibilityFocusable(childNode, childView)) { continue; } if (isSpeakingNode(childNode, childView)) { return true; } } finally { childNode.recycle(); } } return false; } /** * Determines if the provided {@link View} and {@link AccessibilityNodeInfoCompat} meet the * criteria for gaining accessibility focus. * * @param view The {@link View} to evaluate * @param node The {@link AccessibilityNodeInfoCompat} to evaluate * @return {@code true} if it is possible to gain accessibility focus */ public static boolean isAccessibilityFocusable( @Nullable AccessibilityNodeInfoCompat node, @Nullable View view) { if (node == null || view == null) { return false; } // Never focus invisible nodes. if (!node.isVisibleToUser()) { return false; } // Always focus "actionable" nodes. if (isActionableForAccessibility(node)) { return true; } // only focus top-level list items with non-actionable speaking children. return isTopLevelScrollItem(node, view) && isSpeakingNode(node, view); } /** * Determines whether the provided {@link View} and {@link AccessibilityNodeInfoCompat} is a * top-level item in a scrollable container. * * @param view The {@link View} to evaluate * @param node The {@link AccessibilityNodeInfoCompat} to evaluate * @return {@code true} if it is a top-level item in a scrollable container. */ public static boolean isTopLevelScrollItem( @Nullable AccessibilityNodeInfoCompat node, @Nullable View view) { if (node == null || view == null) { return false; } View parent = (View) ViewCompat.getParentForAccessibility(view); if (parent == null) { return false; } if (node.isScrollable()) { return true; } List actionList = node.getActionList(); if (actionList.contains(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD) || actionList.contains(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD)) { return true; } // AdapterView, ScrollView, and HorizontalScrollView are focusable // containers, but Spinner is a special case. if (parent instanceof Spinner) { return false; } return parent instanceof AdapterView || parent instanceof ScrollView || parent instanceof HorizontalScrollView; } /** * Returns whether a node is actionable. That is, the node supports one of * {@link AccessibilityNodeInfoCompat#isClickable()}, * {@link AccessibilityNodeInfoCompat#isFocusable()}, or * {@link AccessibilityNodeInfoCompat#isLongClickable()}. * * @param node The {@link AccessibilityNodeInfoCompat} to evaluate * @return {@code true} if node is actionable. */ public static boolean isActionableForAccessibility(@Nullable AccessibilityNodeInfoCompat node) { if (node == null) { return false; } if (node.isClickable() || node.isLongClickable() || node.isFocusable()) { return true; } List actionList = node.getActionList(); return actionList.contains(AccessibilityNodeInfoCompat.ACTION_CLICK) || actionList.contains(AccessibilityNodeInfoCompat.ACTION_LONG_CLICK) || actionList.contains(AccessibilityNodeInfoCompat.ACTION_FOCUS); } /** * Determines if any of the provided {@link View}'s and {@link AccessibilityNodeInfoCompat}'s * ancestors can receive accessibility focus * * @param view The {@link View} to evaluate * @param node The {@link AccessibilityNodeInfoCompat} to evaluate * @return {@code true} if an ancestor of may receive accessibility focus */ public static boolean hasFocusableAncestor( @Nullable AccessibilityNodeInfoCompat node, @Nullable View view) { if (node == null || view == null) { return false; } ViewParent parentView = ViewCompat.getParentForAccessibility(view); if (!(parentView instanceof View)) { return false; } AccessibilityNodeInfoCompat parentNode = AccessibilityNodeInfoCompat.obtain(); try { ViewCompat.onInitializeAccessibilityNodeInfo((View) parentView, parentNode); if (parentNode == null) { return false; } if (isAccessibilityFocusable(parentNode, (View) parentView)) { return true; } if (hasFocusableAncestor(parentNode, (View) parentView)) { return true; } } finally { parentNode.recycle(); } return false; } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/common/android/DialogFragmentAccessor.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.common.android; import android.app.Dialog; public interface DialogFragmentAccessor extends FragmentAccessor { Dialog getDialog(DIALOG_FRAGMENT dialogFragment); } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/common/android/FragmentAccessor.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.common.android; import android.content.res.Resources; import android.view.View; import javax.annotation.Nullable; public interface FragmentAccessor { int NO_ID = 0; @Nullable FRAGMENT_MANAGER getFragmentManager(FRAGMENT fragment); Resources getResources(FRAGMENT fragment); int getId(FRAGMENT fragment); @Nullable String getTag(FRAGMENT fragment); @Nullable View getView(FRAGMENT fragment); @Nullable FRAGMENT_MANAGER getChildFragmentManager(FRAGMENT fragment); } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/common/android/FragmentActivityAccessor.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.common.android; import android.app.Activity; import javax.annotation.Nullable; public interface FragmentActivityAccessor< FRAGMENT_ACTIVITY extends Activity, FRAGMENT_MANAGER> { @Nullable FRAGMENT_MANAGER getFragmentManager(FRAGMENT_ACTIVITY activity); } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/common/android/FragmentCompat.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.common.android; import android.app.Activity; import com.facebook.stetho.common.ReflectionUtil; import java.lang.reflect.Field; import java.util.List; import javax.annotation.Nullable; import javax.annotation.concurrent.NotThreadSafe; /** * Compatibility abstraction which allows us to generalize access to both the * support library's fragments and the built-in framework version. Note: both versions * can be live at the same time in a single application and even on a single object * instance. *

* Type safety is enforced via generics internal to the implementation but treated * as opaque from the outside. * * @param * @param * @param * @param */ @NotThreadSafe public abstract class FragmentCompat< FRAGMENT, DIALOG_FRAGMENT, FRAGMENT_MANAGER, FRAGMENT_ACTIVITY extends Activity> { private static FragmentCompat sFrameworkInstance; private static FragmentCompat sSupportInstance; private static final boolean sHasSupportFragment; static { sHasSupportFragment = ReflectionUtil.tryGetClassForName( "androidx.fragment.app.Fragment") != null; } @Nullable public static FragmentCompat getFrameworkInstance() { if (sFrameworkInstance == null) { sFrameworkInstance = new FragmentCompatFramework(); } return sFrameworkInstance; } @Nullable public static FragmentCompat getSupportLibInstance() { if (sSupportInstance == null && sHasSupportFragment) { sSupportInstance = new FragmentCompatSupportLib(); } return sSupportInstance; } FragmentCompat() { } public abstract Class getFragmentClass(); public abstract Class getDialogFragmentClass(); public abstract Class getFragmentActivityClass(); public abstract FragmentAccessor forFragment(); public abstract DialogFragmentAccessor forDialogFragment(); public abstract FragmentManagerAccessor forFragmentManager(); public abstract FragmentActivityAccessor forFragmentActivity(); static class FragmentManagerAccessorViaReflection implements FragmentManagerAccessor { @Nullable private Field mFieldMAdded; @SuppressWarnings("unchecked") @Nullable @Override public List getAddedFragments(FRAGMENT_MANAGER fragmentManager) { // This field is actually sitting on FragmentManagerImpl, which derives from FragmentManager. if (mFieldMAdded == null) { Field fieldMAdded = ReflectionUtil.tryGetDeclaredField( fragmentManager.getClass(), "mAdded"); if (fieldMAdded != null) { fieldMAdded.setAccessible(true); mFieldMAdded = fieldMAdded; } } return (mFieldMAdded != null) ? (List)ReflectionUtil.getFieldValue(mFieldMAdded, fragmentManager) : null; } } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/common/android/FragmentCompatFramework.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.common.android; import android.annotation.TargetApi; import android.app.Activity; import android.app.Dialog; import android.app.DialogFragment; import android.app.Fragment; import android.app.FragmentManager; import android.content.res.Resources; import android.os.Build; import android.view.View; import javax.annotation.Nullable; final class FragmentCompatFramework extends FragmentCompat { private static final FragmentAccessorFrameworkHoneycomb sFragmentAccessor; private static final DialogFragmentAccessorFramework sDialogFragmentAccessor; private static final FragmentManagerAccessorViaReflection sFragmentManagerAccessor = new FragmentManagerAccessorViaReflection<>(); private static final FragmentActivityAccessorFramework sFragmentActivityAccessor = new FragmentActivityAccessorFramework(); static { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { sFragmentAccessor = new FragmentAccessorFrameworkJellyBean(); } else { sFragmentAccessor = new FragmentAccessorFrameworkHoneycomb(); } sDialogFragmentAccessor = new DialogFragmentAccessorFramework(sFragmentAccessor); } @Override public Class getFragmentClass() { return Fragment.class; } @Override public Class getDialogFragmentClass() { return DialogFragment.class; } @Override public Class getFragmentActivityClass() { return Activity.class; } @Override public FragmentAccessorFrameworkHoneycomb forFragment() { return sFragmentAccessor; } @Override public DialogFragmentAccessorFramework forDialogFragment() { return sDialogFragmentAccessor; } @Override public FragmentManagerAccessorViaReflection forFragmentManager() { return sFragmentManagerAccessor; } @Override public FragmentActivityAccessorFramework forFragmentActivity() { return sFragmentActivityAccessor; } private static class FragmentAccessorFrameworkHoneycomb implements FragmentAccessor { @Nullable @Override public FragmentManager getFragmentManager(Fragment fragment) { return fragment.getFragmentManager(); } @Override public Resources getResources(Fragment fragment) { return fragment.getResources(); } @Override public int getId(Fragment fragment) { return fragment.getId(); } @Nullable @Override public String getTag(Fragment fragment) { return fragment.getTag(); } @Nullable @Override public View getView(Fragment fragment) { return fragment.getView(); } @Nullable @Override public FragmentManager getChildFragmentManager(Fragment fragment) { return null; } } @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) private static class FragmentAccessorFrameworkJellyBean extends FragmentAccessorFrameworkHoneycomb { @Nullable @Override public FragmentManager getChildFragmentManager(Fragment fragment) { return fragment.getChildFragmentManager(); } } private static class DialogFragmentAccessorFramework implements DialogFragmentAccessor { private final FragmentAccessor mFragmentAccessor; public DialogFragmentAccessorFramework( FragmentAccessor fragmentAccessor) { mFragmentAccessor = fragmentAccessor; } @Override public Dialog getDialog(DialogFragment dialogFragment) { return dialogFragment.getDialog(); } @Nullable @Override public FragmentManager getFragmentManager(Fragment fragment) { return mFragmentAccessor.getFragmentManager(fragment); } @Override public Resources getResources(Fragment fragment) { return mFragmentAccessor.getResources(fragment); } @Override public int getId(Fragment fragment) { return mFragmentAccessor.getId(fragment); } @Nullable @Override public String getTag(Fragment fragment) { return mFragmentAccessor.getTag(fragment); } @Nullable @Override public View getView(Fragment fragment) { return mFragmentAccessor.getView(fragment); } @Nullable @Override public FragmentManager getChildFragmentManager(Fragment fragment) { return mFragmentAccessor.getChildFragmentManager(fragment); } } private static class FragmentActivityAccessorFramework implements FragmentActivityAccessor { @Nullable @Override public FragmentManager getFragmentManager(Activity activity) { return activity.getFragmentManager(); } } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/common/android/FragmentCompatSupportLib.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.common.android; import android.app.Dialog; import android.content.res.Resources; import android.view.View; import javax.annotation.Nullable; import androidx.fragment.app.DialogFragment; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentManager; final class FragmentCompatSupportLib extends FragmentCompat { private static final FragmentAccessorSupportLib sFragmentAccessor = new FragmentAccessorSupportLib(); private static final DialogFragmentAccessorSupportLib sDialogFragmentAccessor = new DialogFragmentAccessorSupportLib(); private static final FragmentManagerAccessorViaReflection sFragmentManagerAccessor = new FragmentManagerAccessorViaReflection<>(); private static final FragmentActivityAccessorSupportLib sFragmentActivityAccessor = new FragmentActivityAccessorSupportLib(); @Override public Class getFragmentClass() { return Fragment.class; } @Override public Class getDialogFragmentClass() { return DialogFragment.class; } @Override public Class getFragmentActivityClass() { return FragmentActivity.class; } @Override public FragmentAccessorSupportLib forFragment() { return sFragmentAccessor; } @Override public DialogFragmentAccessorSupportLib forDialogFragment() { return sDialogFragmentAccessor; } @Override public FragmentManagerAccessor forFragmentManager() { return sFragmentManagerAccessor; } @Override public FragmentActivityAccessorSupportLib forFragmentActivity() { return sFragmentActivityAccessor; } private static class FragmentAccessorSupportLib implements FragmentAccessor { @Nullable @Override public FragmentManager getFragmentManager(Fragment fragment) { return fragment.getFragmentManager(); } @Override public Resources getResources(Fragment fragment) { return fragment.getResources(); } @Override public int getId(Fragment fragment) { return fragment.getId(); } @Nullable @Override public String getTag(Fragment fragment) { return fragment.getTag(); } @Nullable @Override public View getView(Fragment fragment) { return fragment.getView(); } @Nullable @Override public FragmentManager getChildFragmentManager(Fragment fragment) { return fragment.getChildFragmentManager(); } } private static class DialogFragmentAccessorSupportLib extends FragmentAccessorSupportLib implements DialogFragmentAccessor { @Override public Dialog getDialog(DialogFragment dialogFragment) { return dialogFragment.getDialog(); } } private static class FragmentActivityAccessorSupportLib implements FragmentActivityAccessor { @Nullable @Override public FragmentManager getFragmentManager(FragmentActivity activity) { return activity.getSupportFragmentManager(); } } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/common/android/FragmentCompatUtil.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.common.android; import android.app.Activity; import android.view.View; import java.util.List; import javax.annotation.Nullable; public final class FragmentCompatUtil { private FragmentCompatUtil() { } public static boolean isDialogFragment(Object fragment) { FragmentCompat supportLib = FragmentCompat.getSupportLibInstance(); if (supportLib != null && supportLib.getDialogFragmentClass().isInstance(fragment)) { return true; } FragmentCompat framework = FragmentCompat.getFrameworkInstance(); if (framework != null && framework.getDialogFragmentClass().isInstance(fragment)) { return true; } return false; } @Nullable public static Object findFragmentForView(View view) { Activity activity = ViewUtil.tryGetActivity(view); if (activity == null) { return null; } return findFragmentForViewInActivity(activity, view); } @Nullable private static Object findFragmentForViewInActivity(Activity activity, View view) { FragmentCompat supportLib = FragmentCompat.getSupportLibInstance(); // Try the support library version if it is present and the activity is FragmentActivity. if (supportLib != null && supportLib.getFragmentActivityClass().isInstance(activity)) { Object fragment = findFragmentForViewInActivity(supportLib, activity, view); if (fragment != null) { return fragment; } } // Try the actual Android runtime version if we are on a sufficiently high API level for it to // exist. Note that technically we can have both the support library and the framework // version in the same object instance due to FragmentActivity extending Activity (which has // fragment support in the system). FragmentCompat framework = FragmentCompat.getFrameworkInstance(); if (framework != null) { Object fragment = findFragmentForViewInActivity(framework, activity, view); if (fragment != null) { return fragment; } } return null; } private static Object findFragmentForViewInActivity( FragmentCompat compat, Activity activity, View view) { Object fragmentManager = compat.forFragmentActivity().getFragmentManager(activity); if (fragmentManager != null) { return findFragmentForViewInFragmentManager(compat, fragmentManager, view); } else { return null; } } @Nullable private static Object findFragmentForViewInFragmentManager( FragmentCompat compat, Object fragmentManager, View view) { List fragments = compat.forFragmentManager().getAddedFragments(fragmentManager); if (fragments != null) { for (int i = 0, N = fragments.size(); i < N; ++i) { Object fragment = fragments.get(i); Object result = findFragmentForViewInFragment(compat, fragment, view); if (result != null) { return result; } } } return null; } @Nullable private static Object findFragmentForViewInFragment( FragmentCompat compat, Object fragment, View view) { FragmentAccessor accessor = compat.forFragment(); if (accessor.getView(fragment) == view) { return fragment; } Object childFragmentManager = accessor.getChildFragmentManager(fragment); if (childFragmentManager != null) { return findFragmentForViewInFragmentManager(compat, childFragmentManager, view); } return null; } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/common/android/FragmentManagerAccessor.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.common.android; import java.util.List; import javax.annotation.Nullable; public interface FragmentManagerAccessor { @Nullable List getAddedFragments(FRAGMENT_MANAGER fragmentManager); } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/common/android/HandlerUtil.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.common.android; import android.os.Handler; import android.os.Looper; import com.facebook.stetho.common.UncheckedCallable; import com.facebook.stetho.common.Util; public final class HandlerUtil { private HandlerUtil() { } /** * Checks whether the current thread is the same thread that the {@link Handler} is associated * with. * @return true if the current thread is the same thread that the {@link Handler} is associated * with; otherwise false. */ public static boolean checkThreadAccess(Handler handler) { return Looper.myLooper() == handler.getLooper(); } /** * Enforces that the current thread is the same thread that the {@link Handler} is associated * with. * @throws IllegalStateException if the current thread is not the same thread that the * {@link Handler} is associated with. */ public static void verifyThreadAccess(Handler handler) { Util.throwIfNot(checkThreadAccess(handler)); } /** * Synchronously executes an {@link UncheckedCallable} on the thread that this Handler is * associated with, and returns its result. * @param c the {@link UncheckedCallable} to execute * @param the return type of the {@link UncheckedCallable} * @return the return value from {@link UncheckedCallable#call()} * @throws RuntimeException if the {@link UncheckedCallable} could not be executed (the cause * will be null), or if {@link UncheckedCallable#call()} threw an exception (the cause will be the * exception that it threw). */ public static V postAndWait(Handler handler, final UncheckedCallable c) { if (checkThreadAccess(handler)) { try { return c.call(); } catch (Exception e) { throw new RuntimeException(e); } } WaitableRunnable wrapper = new WaitableRunnable() { @Override protected V onRun() { return c.call(); } }; return wrapper.invoke(handler); } /** * Synchronously executes a {@link Runnable} on the thread that this Handler is associated with. * @param r the {@link Runnable} to execute * @throws RuntimeException if the {@link Runnable} could not be executed (the cause will be * null), or if {@link Runnable#run()} threw an exception (the cause will be the exception that * it threw). */ public static void postAndWait(Handler handler, final Runnable r) { if (checkThreadAccess(handler)) { try { r.run(); return; } catch (RuntimeException e) { throw new RuntimeException(e); } } WaitableRunnable wrapper = new WaitableRunnable() { @Override protected Void onRun() { r.run(); return null; } }; wrapper.invoke(handler); } private static abstract class WaitableRunnable implements Runnable { private boolean mIsDone; private V mValue; private Exception mException; protected WaitableRunnable() { } @Override public final void run() { try { mValue = onRun(); mException = null; } catch (Exception e) { mValue = null; mException = e; } finally { synchronized (this) { mIsDone = true; notifyAll(); } } } protected abstract V onRun(); public V invoke(Handler handler) { if (!handler.post(this)) { throw new RuntimeException("Handler.post() returned false"); } join(); if (mException != null) { throw new RuntimeException(mException); } return mValue; } private void join() { synchronized (this) { while (!mIsDone) { try { wait(); } catch (InterruptedException e) { } } } } } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/common/android/ResourcesUtil.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.common.android; import android.content.res.Resources; import com.facebook.stetho.common.LogUtil; import javax.annotation.Nonnull; import javax.annotation.Nullable; public class ResourcesUtil { private ResourcesUtil() { } @Nonnull public static String getIdStringQuietly(Object idContext, @Nullable Resources r, int resourceId) { try { return getIdString(r, resourceId); } catch (Resources.NotFoundException e) { String idString = getFallbackIdString(resourceId); LogUtil.w("Unknown identifier encountered on " + idContext + ": " + idString); return idString; } } public static String getIdString(@Nullable Resources r, int resourceId) throws Resources.NotFoundException { if (r == null) { return getFallbackIdString(resourceId); } String prefix; String prefixSeparator; switch (getResourcePackageId(resourceId)) { case 0x7f: prefix = ""; prefixSeparator = ""; break; default: prefix = r.getResourcePackageName(resourceId); prefixSeparator = ":"; break; } String typeName = r.getResourceTypeName(resourceId); String entryName = r.getResourceEntryName(resourceId); StringBuilder sb = new StringBuilder( 1 + prefix.length() + prefixSeparator.length() + typeName.length() + 1 + entryName.length()); sb.append("@"); sb.append(prefix); sb.append(prefixSeparator); sb.append(typeName); sb.append("/"); sb.append(entryName); return sb.toString(); } private static String getFallbackIdString(int resourceId) { return "#" + Integer.toHexString(resourceId); } private static int getResourcePackageId(int id) { return (id >>> 24) & 0xff; } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/common/android/ViewGroupUtil.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.common.android; import android.view.View; import android.view.ViewGroup; public final class ViewGroupUtil { private ViewGroupUtil() { } public static int findChildIndex(ViewGroup parent, View child) { int count = parent.getChildCount(); for (int i = 0; i < count; ++i) { if (parent.getChildAt(i) == child) { return i; } } return -1; } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/common/android/ViewUtil.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.common.android; import android.app.Activity; import android.content.Context; import android.content.ContextWrapper; import android.view.View; import android.view.ViewParent; import javax.annotation.Nullable; final class ViewUtil { private ViewUtil() { } @Nullable static Activity tryGetActivity(View view) { if (view == null) { return null; } Context context = view.getContext(); Activity activityFromContext = tryGetActivity(context); if (activityFromContext != null) { return activityFromContext; } ViewParent parent = view.getParent(); if (parent instanceof View) { View parentView = (View)parent; return tryGetActivity(parentView); } return null; } @Nullable private static Activity tryGetActivity(Context context) { while (context != null) { if (context instanceof Activity) { return (Activity) context; } else if (context instanceof ContextWrapper) { context = ((ContextWrapper) context).getBaseContext(); } else { return null; } } return null; } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/dumpapp/ArgsHelper.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.dumpapp; import java.util.ArrayList; import java.util.Iterator; import java.util.List; public class ArgsHelper { public static String nextOptionalArg(Iterator argsIter, String defaultValue) { return argsIter.hasNext() ? argsIter.next() : defaultValue; } public static String nextArg(Iterator argsIter, String errorIfMissing) throws DumpUsageException { if (!argsIter.hasNext()) { throw new DumpUsageException(errorIfMissing); } return argsIter.next(); } public static String[] drainToArray(Iterator argsIter) { List args = new ArrayList<>(); while (argsIter.hasNext()) { args.add(argsIter.next()); } return args.toArray(new String[args.size()]); } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/dumpapp/DumpException.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.dumpapp; import org.apache.commons.cli.ParseException; /** * Exception thrown if there is a functional issue executing the specified commands. This is * not thrown on {@link ParseException} (which represents a command-line syntax issue). *

* This exception's message should be human readable and will be printed to the dumpapp * caller. dumpapp will also exit with a non-zero exit code. */ public class DumpException extends Exception { public DumpException(String message) { super(message); } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/dumpapp/DumpUsageException.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.dumpapp; /** * Usage error in a {@link DumperPlugin}. */ public class DumpUsageException extends DumpException { public DumpUsageException(String message) { super(message); } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/dumpapp/DumpappFramingException.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.dumpapp; import java.io.IOException; /** * Thrown to indicate an error in the dumpapp framing protocol as received from the remote * peer. */ class DumpappFramingException extends IOException { public DumpappFramingException(String message) { super(message); } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/dumpapp/DumpappHttpSocketLikeHandler.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.dumpapp; import com.facebook.stetho.server.SocketLike; import com.facebook.stetho.server.SocketLikeHandler; 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.http.LightHttpBody; import com.facebook.stetho.server.http.LightHttpRequest; import com.facebook.stetho.server.http.LightHttpResponse; import com.facebook.stetho.server.http.LightHttpServer; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.List; /** * Provides a legacy hook to support old dumpapp scripts which have not been * updated to the new binary protocol. Since the framing protocol is still compatible this * can be easily supported to reduce upgrade friction since so many developers forgot to * copy the {@code dumpapp} script on upgrade. * *

{@code stdin} is not supported with this legacy mode. Streaming output is also not * supported.

* *

This script sends a warning to stderr informing the developer of its deprecated nature.

*/ @Deprecated public class DumpappHttpSocketLikeHandler implements SocketLikeHandler { private final LightHttpServer mServer; public DumpappHttpSocketLikeHandler(Dumper dumper) { HandlerRegistry registry = new HandlerRegistry(); registry.register( new ExactPathMatcher("/dumpapp"), new DumpappLegacyHttpHandler(dumper)); mServer = new LightHttpServer(registry); } @Override public void onAccepted(SocketLike socket) throws IOException { mServer.serve(socket); } private static class DumpappLegacyHttpHandler implements HttpHandler { private static final String QUERY_PARAM_ARGV = "argv"; private static final String RESPONSE_HEADER_ALLOW_ORIGIN = "Access-Control-Allow-Origin"; private static final String CONTENT_TYPE = "application/octet-stream"; private final Dumper mDumper; public DumpappLegacyHttpHandler(Dumper dumper) { mDumper = dumper; } @Override public boolean handleRequest( SocketLike socket, LightHttpRequest request, LightHttpResponse response) throws IOException { boolean postMethod = "POST".equals(request.method); boolean getMethod = !postMethod && "GET".equals(request.method); if (getMethod || postMethod) { List argv = request.uri.getQueryParameters(QUERY_PARAM_ARGV); ByteArrayOutputStream outputBuffer = new ByteArrayOutputStream(); Framer framer = new Framer( new ByteArrayInputStream(new byte[0]), outputBuffer); String warningPrefix = postMethod ? "ERROR" : "WARNING"; framer.getStderr().println( "*** " + warningPrefix + ": Using legacy HTTP protocol; update dumpapp script! ***"); if (getMethod) { DumpappSocketLikeHandler.dump(mDumper, framer, argv.toArray(new String[argv.size()])); } else { // stdin access is completely unsupported, so we can't allow any dumper plugins // to run... framer.writeExitCode(1); } response.code = HttpStatus.HTTP_OK; response.reasonPhrase = "OK"; response.addHeader(RESPONSE_HEADER_ALLOW_ORIGIN, "*"); response.body = LightHttpBody.create(outputBuffer.toByteArray(), CONTENT_TYPE); } else { response.code = HttpStatus.HTTP_NOT_IMPLEMENTED; response.reasonPhrase = "Not implemented"; response.body = LightHttpBody.create(request.method + " not implemented", "text/plain"); } return true; } } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/dumpapp/DumpappOutputBrokenException.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.dumpapp; import java.io.IOException; import java.io.OutputStream; import java.io.PrintStream; /** * When streaming output, it is common for the user to just hit Ctrl-C * to terminate the stream. When this happens, the underlying output * stream throws an {@link IOException} to indicate the pipe is broken. * Dumpapp uses a {@link PrintStream} to wrap the underlying {@link OutputStream} * though, and {@link PrintStream} silently swallows {@link IOException}. *

* While streaming dumpers can/should check {@link PrintStream#checkError}, * this is used in cases where we know the stream has gone bad to force flow * control out of the dumper and back into the calling machinery that controls * the stream framer. */ class DumpappOutputBrokenException extends RuntimeException { public DumpappOutputBrokenException() { } public DumpappOutputBrokenException(String detailMessage) { super(detailMessage); } public DumpappOutputBrokenException(String detailMessage, Throwable throwable) { super(detailMessage, throwable); } public DumpappOutputBrokenException(Throwable throwable) { super(throwable); } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/dumpapp/DumpappSocketLikeHandler.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.dumpapp; import com.facebook.stetho.common.LogUtil; import com.facebook.stetho.server.SocketLike; import com.facebook.stetho.server.SocketLikeHandler; import java.io.DataInputStream; import java.io.IOException; import java.util.Arrays; /** * Provides a kind of CLI-over-HTTP support for the ./scripts/dumpapp tool. *

* This handler accepts a list of text-based arguments to a FAB endpoint and responds with * a stream as furnished by the Dumper implementation on the app side. A special "exit code" * property is also returned that the dumpapp tool uses to pass along the exit code of the * script. */ public class DumpappSocketLikeHandler implements SocketLikeHandler { public static final byte[] PROTOCOL_MAGIC = new byte[] { 'D', 'U', 'M', 'P' }; public static final int PROTOCOL_VERSION = 1; private final Dumper mDumper; public DumpappSocketLikeHandler(Dumper dumper) { mDumper = dumper; } @Override public void onAccepted(SocketLike socket) throws IOException { DataInputStream in = new DataInputStream(socket.getInput()); // Get through the initial hello... establishConversation(in); Framer framer = new Framer(in, socket.getOutput()); String[] args = readArgs(framer); dump(mDumper, framer, args); } static void dump(Dumper dumper, Framer framer, String[] args) throws IOException { try { // We intentionally do not catch-all and write an exit code here. // // The dumper catches all expected exceptions and translates // them to an exit code, so the normal case is all good. // // DumpappOutputBrokenException is thrown in cases where we know // we are unable to write any more, including possibly while // writing the error code itself. // // Because other unchecked exceptions could also be thrown in // cases where the underlying stream is broken, and making // further calls on the broken stream (to write an exit code) // can corrupt the stream and throw still more unchecked // exceptions, we cannot safely write an exit code in this case. int exitCode = dumper.dump( framer.getStdin(), framer.getStdout(), framer.getStderr(), args); framer.writeExitCode(exitCode); } catch (DumpappOutputBrokenException e) { // This exception indicates we must stop all writes to the underlying stream // because there was IOException. We interpret this to mean that we should // also shutdown the whole pipeline, similar to how SIGPIPE would behave // for command-line apps. } } private void establishConversation(DataInputStream in) throws IOException { byte[] magic = new byte[4]; in.readFully(magic); if (!Arrays.equals(PROTOCOL_MAGIC, magic)) { throw logAndThrowProtocolException( "Incompatible protocol, are you using an old dumpapp script?"); } int version = in.readInt(); if (version != PROTOCOL_VERSION) { throw logAndThrowProtocolException( "Expected version=" + PROTOCOL_VERSION + "; got=" + version); } } private static IOException logAndThrowProtocolException(String message) throws IOException { LogUtil.w(message); throw new IOException(message); } private String[] readArgs(Framer framer) throws IOException { synchronized (framer) { byte type = framer.readFrameType(); switch (type) { case Framer.ENTER_FRAME_PREFIX: int argc = framer.readInt(); String[] argv = new String[argc]; for (int i = 0; i < argc; i++) { argv[i] = framer.readString(); } return argv; default: throw new DumpappFramingException("Expected enter frame, got: " + type); } } } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/dumpapp/Dumper.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.dumpapp; import java.io.InputStream; import java.io.PrintStream; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.CommandLineParser; import org.apache.commons.cli.GnuParser; import org.apache.commons.cli.HelpFormatter; import org.apache.commons.cli.ParseException; public class Dumper { /** * Map of dumper plugin name to the plugin itself. */ private final Map mDumperPlugins; private final CommandLineParser mParser; private final GlobalOptions mGlobalOptions; public Dumper(Iterable dumperPlugins) { this(dumperPlugins, new GnuParser()); } public Dumper( Iterable dumperPlugins, CommandLineParser parser) { mDumperPlugins = generatePluginMap(dumperPlugins); mParser = parser; mGlobalOptions = new GlobalOptions(); } private static Map generatePluginMap(Iterable plugins) { Map map = new HashMap(); for (DumperPlugin plugin : plugins) { map.put(plugin.getName(), plugin); } return Collections.unmodifiableMap(map); } /** * Execute the dump method as if it were a fragile external command. Swallows exceptions * and translates them to dumped output text. * * @param input Input that came from the CLI's stdin. * @param out Output that will eventually make its way to the CLI's stdout. * @param err Output that will eventually make its way to the CLI's stderr. * @param args Argument list as passed from the CLI. * * @return Exit code as if this were a command-line invocation. */ public int dump(InputStream input, PrintStream out, PrintStream err, String[] args) { try { return doDump(input, out, err, args); } catch (ParseException e) { err.println(e.getMessage()); dumpUsage(err); return 1; } catch (DumpException e) { err.println(e.getMessage()); return 1; } catch (DumpappOutputBrokenException e) { // The peer is already gone, no sense in sending the exception stack to them. throw e; } catch (RuntimeException e) { e.printStackTrace(err); return 1; } } /** * @throws ParseException If any args syntax constraint is violated and the dump was not able to * proceed. * @throws DumpException Human readable error executing the dump command. */ private int doDump(InputStream input, PrintStream out, PrintStream err, String[] args) throws ParseException, DumpException { CommandLine parsedArgs = mParser.parse(mGlobalOptions.options, args, true /* stopAtNonOption */); if (parsedArgs.hasOption(mGlobalOptions.optionHelp.getOpt())) { dumpUsage(out); return 0; } else if (parsedArgs.hasOption(mGlobalOptions.optionListPlugins.getOpt())) { dumpAvailablePlugins(out); return 0; } else if (!parsedArgs.getArgList().isEmpty()) { dumpPluginOutput(input, out, err, parsedArgs); return 0; } else { // Didn't understand the options, spit out help but use a non-success exit code. dumpUsage(err); return 1; } } private void dumpAvailablePlugins(PrintStream output) { List pluginNames = new ArrayList(); for (DumperPlugin pluginToDump : mDumperPlugins.values()) { pluginNames.add(pluginToDump.getName()); } Collections.sort(pluginNames); for (String pluginName : pluginNames) { output.println(pluginName); } } private void dumpPluginOutput(InputStream input, PrintStream out, PrintStream err, CommandLine parsedArgs) throws DumpException { List args = new ArrayList(parsedArgs.getArgList()); if (args.size() < 1) { throw new DumpException("Expected plugin argument"); } String pluginName = args.remove(0); DumperPlugin plugin = mDumperPlugins.get(pluginName); if (plugin == null) { throw new DumpException("No plugin named '" + pluginName + "'"); } DumperContext dumperContext = new DumperContext(input, out, err, mParser, args); plugin.dump(dumperContext); } private void dumpUsage(PrintStream output) { final String cmdName = "dumpapp"; HelpFormatter formatter = new HelpFormatter(); output.println("Usage: " + cmdName + " [options] [plugin-options]"); PrintWriter writer = new PrintWriter(output); try { formatter.printOptions( writer, formatter.getWidth(), mGlobalOptions.options, formatter.getLeftPadding(), formatter.getDescPadding()); } finally { writer.flush(); } } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/dumpapp/DumperContext.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.dumpapp; import javax.annotation.concurrent.Immutable; import java.io.InputStream; import java.io.PrintStream; import java.util.List; import com.facebook.stetho.common.Util; import org.apache.commons.cli.CommandLineParser; @Immutable public class DumperContext { private final InputStream mStdin; private final PrintStream mStdout; private final PrintStream mStderr; private final CommandLineParser mParser; private final List mArgs; /** * Construct a new context instance using a new set of remaining arguments after invoking * {@link CommandLineParser#parse}. */ protected DumperContext( DumperContext existingContext, List newRemainingArguments) { this( existingContext.getStdin(), existingContext.getStdout(), existingContext.getStderr(), existingContext.getParser(), newRemainingArguments); } public DumperContext( InputStream stdin, PrintStream stdout, PrintStream stderr, CommandLineParser parser, List args) { mStdin = Util.throwIfNull(stdin); mStdout = Util.throwIfNull(stdout); mStderr = Util.throwIfNull(stderr); mParser = Util.throwIfNull(parser); mArgs = Util.throwIfNull(args); } /** * Access the caller's stdin input stream. This stream should only be read once (do not rely on * it having been buffered fully). */ public InputStream getStdin() { return mStdin; } /** * Access the caller's stdout output stream. */ public PrintStream getStdout() { return mStdout; } /** * Access the caller's stderr output stream. */ public PrintStream getStderr() { return mStderr; } public CommandLineParser getParser() { return mParser; } public List getArgsAsList() { return mArgs; } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/dumpapp/DumperPlugin.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.dumpapp; /** * Provides a CLI (command-line interface) facility for {@code fbandroid} modules. * *

Binding an instance of this interface allows you to inject custom debug/dumping code * into the app that is accessible when a phone running the app is connected via ADB. To see a * list of current plugins, see: *

 *   ./scripts/dumpapp --list
 * 
* *

General Utility

* *

This system allows for complex components to inform developers of what's been going on * recently (or over the process lifetime) in a simple, human-readable fashion. Good candidates * for this kind of interface are those that are otherwise hard to isolate or visualize such as * the network stack or task runners (BlueService, Executor, etc). It is also possible to create * a kind of simple instrumentation around components that otherwise have no UI. For example, it * may be useful to be able to clear the image cache or delete individual entries on demand as * well as viewing the current state of all cached entries. * *

Implementation Guidelines

* * Almost any kind of simple interface will work however there are some basic guidelines that you * should follow: * *
    *
  1. Avoid heavy-weight plugins that add significant additional code to the app. While this * code does not ship in katana (proguard strips it out), it is generally a good idea to evaluate * large or complex instrumentation projects in the greater context of tools available at Facebook * (in particular Scuba which can aggregate data for our user base in the wild). 1 or 2 classes * is a good rule of thumb for reasonable plugin size. *
  2. Don't implement long-running jobs in {@link #dump}. If the instrumented action takes a * long time, consider a way to "fire-and-forget" so that you can return some message to the * caller quickly. *
  3. Don't perform extra up-front initialization not normally needed by the app. Dumpers should * init on demand so that there is no runtime impact to the app. *
* *

Gotchas

* *

In order for dumpapp to work the app must currently be running and there is an * initialization overhead of the "dumper" system which may make real-time debug output during * app startup tricky or impossible. However, you can work around this by implementing simple * stat counters that can be pulled and reset within your plugin. After initialization completes * (and thus dumpapp becomes available), you can query for the stats that were being collected * while the app was starting. */ public interface DumperPlugin { /** * Plugin name according to the dumpsys command-line interface. Please be mindful of the * fact that this is part of a command-line interface and should be generally easy to type and * remember. Avoid underscores, capital letters, and plural names. Instead, prefer terse names * like "network", "logging", etc. */ String getName(); /** * Invoked in response to the user running the dumpapp command and specifying your plugin. *

* Any output written to {@link DumperContext#getStdout()} will be displayed to the caller. * * @param dumpContext Contains the command-line state (extra arguments, output channel, etc). * @throws DumpException Your plugin can throw this event to easily bail from a dump sequence * on unexpected errors. The message will be displayed directly to the caller and the * dumpapp script will terminate with a non-successful exit code. */ void dump(DumperContext dumpContext) throws DumpException; } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/dumpapp/Framer.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.dumpapp; import com.facebook.stetho.common.LogUtil; import java.io.BufferedOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintStream; import java.nio.charset.Charset; import javax.annotation.Nullable; /** * Implements framing protocol that allows us to implement a command-line protocol via * sockets complete with stdin/stdout/stderr, exit codes, and input arguments. *

* This is the server handler of that protocol, with the client handler in Stetho's {@code dumpapp} * script. *

* The framing protocol involves 5-byte fixed headers, possibly followed by a variable * size content body. * The grammar is: *

 *   CLIENT_FRAME = STDIN_FRAME | ENTER_FRAME
 *   SERVER_FRAME = STDIN_REQUEST_FRAME | STDOUT_FRAME | STDERR_FRAME | EXIT_FRAME
 *   STDIN_REQUEST_FRAME = '_' BIG_ENDIAN_INT
 *   STDIN_FRAME = '-' BIG_ENDIAN_INT BLOB
 *   STDOUT_FRAME = '1' BIG_ENDIAN_INT BLOB
 *   STDERR_FRAME = '2' BIG_ENDIAN_INT BLOB
 *   ENTER_FRAME = '!' BIG_ENDIAN_INT [ BIG_ENDIAN_SHORT STRING ]...
 *   EXIT_FRAME = 'x' BIG_ENDIAN_INT
 *   BIG_ENDIAN_SHORT = (2 bytes as written by {@link DataOutputStream#writeShort})
 *   BIG_ENDIAN_INT = (4 bytes as written by {@link DataOutputStream#writeInt})
 *   BLOB = (variable-size byte array)
 *   STRING = (variable-size UTF8 string)
 * 
* The BIG_ENDIAN_INT in STDIN/STDOUT/STDERR_FRAME specifies the size (in bytes) of * the immediately following BLOB. For STDIN_REQUEST_FRAME it represents a request * for that much data. *

* The BIG_ENDIAN_INT in ENTER_FRAME specifies the number of arguments, with that number of string * to follow. *

* The BIG_ENDIAN_INT in EXIT_FRAME specifies the exit code. */ class Framer { private static final String TAG = "FramingSocket"; public static final byte STDIN_FRAME_PREFIX = '-'; public static final byte STDIN_REQUEST_FRAME_PREFIX = '_'; public static final byte STDOUT_FRAME_PREFIX = '1'; public static final byte STDERR_FRAME_PREFIX = '2'; public static final byte ENTER_FRAME_PREFIX = '!'; public static final byte EXIT_FRAME_PREFIX = 'x'; private final DataInputStream mInput; private final InputStream mStdin; private final PrintStream mStdout; private final PrintStream mStderr; private final DataOutputStream mMultiplexedOutputStream; public Framer(InputStream input, OutputStream output) throws IOException { mInput = new DataInputStream(input); mMultiplexedOutputStream = new DataOutputStream(output); mStdin = new FramingInputStream(); mStdout = new PrintStream( new BufferedOutputStream( new FramingOutputStream(STDOUT_FRAME_PREFIX))); mStderr = new PrintStream( new FramingOutputStream(STDERR_FRAME_PREFIX)); } public InputStream getStdin() { return mStdin; } public PrintStream getStdout() { return mStdout; } public PrintStream getStderr() { return mStderr; } public byte readFrameType() throws IOException { return mInput.readByte(); } public int readInt() throws IOException { return mInput.readInt(); } public String readString() throws IOException { int size = mInput.readUnsignedShort(); byte[] buf = new byte[size]; mInput.readFully(buf); return new String(buf, Charset.forName("UTF-8")); } public void writeExitCode(int exitCode) throws IOException { mStdout.flush(); mStderr.flush(); writeIntFrame(EXIT_FRAME_PREFIX, exitCode); } public void writeIntFrame(byte type, int intParameter) throws IOException { mMultiplexedOutputStream.write(type); mMultiplexedOutputStream.writeInt(intParameter); } public void writeBlob(byte[] data, int offset, int count) throws IOException { mMultiplexedOutputStream.write(data, offset, count); } private static T handleSuppression(@Nullable T previous, T current) { if (previous == null) { return current; } else { LogUtil.i(TAG, current, "Suppressed while handling " + previous); return previous; } } private class FramingInputStream extends InputStream { private final ClosedHelper mClosedHelper = new ClosedHelper(); @Override public int read() throws IOException { byte[] buf = new byte[1]; if (read(buf) == 0) { return -1; } return buf[0]; } @Override public int read(byte[] buffer) throws IOException { return read(buffer, 0, buffer.length); } @Override public int read(byte[] buffer, int byteOffset, int byteCount) throws IOException { mClosedHelper.throwIfClosed(); synchronized (Framer.this) { // Ask the client for more data... writeIntFrame(STDIN_REQUEST_FRAME_PREFIX, byteCount); byte b = readFrameType(); if (b != STDIN_FRAME_PREFIX) { throw new UnexpectedFrameException(STDIN_FRAME_PREFIX, b); } // Read what they gave us... int length = readInt(); if (length > 0) { if (length > byteCount) { throw new DumpappFramingException( "Expected at most " + byteCount + " bytes, got: " + length); } mInput.readFully(buffer, byteOffset, length); } return length; } } @Override public long skip(long byteCount) throws IOException { long skipped = 0; int bufSize = (int)Math.min(byteCount, 2048); byte[] buf = new byte[bufSize]; synchronized (Framer.this) { while (skipped < byteCount) { int n = read(buf); if (n < 0) { break; } skipped += n; } } return skipped; } @Override public void close() throws IOException { mClosedHelper.close(); } } private class FramingOutputStream extends OutputStream { private final byte mPrefix; private final ClosedHelper mClosedHelper = new ClosedHelper(); public FramingOutputStream(byte prefix) { mPrefix = prefix; } @Override public void write(byte[] buffer, int offset, int length) throws IOException { mClosedHelper.throwIfClosed(); if (length > 0) { try { synchronized (Framer.this) { writeIntFrame(mPrefix, length); writeBlob(buffer, offset, length); mMultiplexedOutputStream.flush(); } } catch (IOException e) { // I/O error here can indicate the pipe is broken, so we need to prevent any // further writes. throw new DumpappOutputBrokenException(e); } } } @Override public void write(int oneByte) throws IOException { byte[] buffer = new byte[] { (byte)oneByte }; write(buffer, 0, buffer.length); } @Override public void write(byte[] buffer) throws IOException { write(buffer, 0, buffer.length); } @Override public void close() throws IOException{ mClosedHelper.close(); } } private static class ClosedHelper { private volatile boolean mClosed; public void throwIfClosed() throws IOException { if (mClosed) { throw new IOException("Stream is closed"); } } public void close() { mClosed = true; } } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/dumpapp/GlobalOptions.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.dumpapp; import org.apache.commons.cli.Option; import org.apache.commons.cli.Options; public class GlobalOptions { public final Option optionHelp = new Option("h", "help", false, "Print this help"); public final Option optionListPlugins = new Option("l", "list", false, "List available plugins"); /** * Special option used to inject it into help but is otherwise not processed within the app. * Instead, the dumpapp shell script interprets this and figures out which port to send the * request to. */ public final Option optionProcess = new Option("p", "process", true, "Specify target process"); public final Options options; public GlobalOptions() { options = new Options(); options.addOption(optionHelp); options.addOption(optionListPlugins); options.addOption(optionProcess); } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/dumpapp/UnexpectedFrameException.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.dumpapp; class UnexpectedFrameException extends DumpappFramingException { public UnexpectedFrameException(byte expected, byte got) { super("Expected '" + expected + "', got: '" + got + "'"); } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/dumpapp/plugins/CrashDumperPlugin.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.dumpapp.plugins; import com.facebook.stetho.common.ExceptionUtil; import com.facebook.stetho.common.Util; import com.facebook.stetho.dumpapp.ArgsHelper; import com.facebook.stetho.dumpapp.DumpException; import com.facebook.stetho.dumpapp.DumpUsageException; import com.facebook.stetho.dumpapp.DumperContext; import com.facebook.stetho.dumpapp.DumperPlugin; import java.io.IOException; import java.io.InputStream; import java.io.PrintStream; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.util.Iterator; import java.util.concurrent.CountDownLatch; import javax.annotation.Nullable; /** * Yes, this intentionally crashes the app. Useful for testing crash recovery and crash reporter * work flows. Three separate exit strategies are supported; see help output for more details. */ public class CrashDumperPlugin implements DumperPlugin { private static final String NAME = "crash"; private static final String OPTION_THROW_DEFAULT = "java.lang.Error"; private static final String OPTION_KILL_DEFAULT = "9"; // SIGKILL private static final String OPTION_EXIT_DEFAULT = "0"; // EXIT_SUCCESS in C public CrashDumperPlugin() { } @Override public String getName() { return NAME; } @Override public void dump(DumperContext dumpContext) throws DumpException { Iterator argsIter = dumpContext.getArgsAsList().iterator(); String command = ArgsHelper.nextOptionalArg(argsIter, null); if ("throw".equals(command)) { doUncaughtException(argsIter); } else if ("kill".equals(command)) { doKill(dumpContext, argsIter); } else if ("exit".equals(command)) { doSystemExit(argsIter); } else { doUsage(dumpContext.getStdout()); if (command != null) { throw new DumpUsageException("Unsupported command: " + command); } } } private void doUsage(PrintStream out) { final String cmdName = "dumpapp " + NAME; String usagePrefix = "Usage: " + cmdName + " "; String blankPrefix = " " + cmdName + " "; out.println(usagePrefix + " [command-options]"); out.println(usagePrefix + "throw"); out.println(blankPrefix + "kill"); out.println(blankPrefix + "exit"); out.println(); out.println(cmdName + " throw: Throw an uncaught exception (simulates a program crash)"); out.println(" : Throwable class to use (default: " + OPTION_THROW_DEFAULT + ")"); out.println(); out.println(cmdName + " kill: Send a signal to this process (simulates the low memory killer)"); out.println(" : Either signal name or number to send (default: " + OPTION_KILL_DEFAULT + ")"); out.println(" See `adb shell kill -l` for more information"); out.println(); out.println(cmdName + " exit: Invoke System.exit (simulates an abnormal Android exit strategy)"); out.println(" : Exit code (default: " + OPTION_EXIT_DEFAULT + ")"); } private void doSystemExit(Iterator argsIter) { String exitCodeStr = ArgsHelper.nextOptionalArg(argsIter, OPTION_EXIT_DEFAULT); System.exit(Integer.parseInt(exitCodeStr)); } private void doKill(DumperContext dumpContext, Iterator argsIter) throws DumpException { String signal = ArgsHelper.nextOptionalArg(argsIter, OPTION_KILL_DEFAULT); try { Process kill = new ProcessBuilder() .command("/system/bin/kill", "-" + signal, String.valueOf(android.os.Process.myPid())) .redirectErrorStream(true) .start(); // Handle kill command output gracefully in the event that the signal delivered didn't // actually take out our process... try { InputStream in = kill.getInputStream(); Util.copy(in, dumpContext.getStdout(), new byte[1024]); } finally { kill.destroy(); } } catch (IOException e) { throw new DumpException("Failed to invoke kill: " + e); } } private void doUncaughtException(Iterator argsIter) throws DumpException { String throwableClassString = ArgsHelper.nextOptionalArg(argsIter, OPTION_THROW_DEFAULT); try { Class throwableClass = (Class)Class.forName(throwableClassString); Throwable t; Constructor ctorWithMessage = tryGetDeclaredConstructor(throwableClass, String.class); if (ctorWithMessage != null) { t = ctorWithMessage.newInstance("Uncaught exception triggered by Stetho"); } else { Constructor ctorParameterless = throwableClass.getDeclaredConstructor(); t = ctorParameterless.newInstance(); } Thread crashThread = new Thread(new ThrowRunnable(t)); crashThread.start(); Util.joinUninterruptibly(crashThread); } catch ( ClassNotFoundException | ClassCastException | NoSuchMethodException | IllegalAccessException | InstantiationException e) { throw new DumpException("Invalid supplied Throwable class: " + e); } catch (InvocationTargetException e) { // This means that the method invoked actually threw, independent of reflection. Best // reflect that as a normal unchecked exception in dumpapp output. throw ExceptionUtil.propagate(e.getCause()); } } @Nullable private static Constructor tryGetDeclaredConstructor( Class clazz, Class... parameterTypes) { try { return clazz.getDeclaredConstructor(parameterTypes); } catch (NoSuchMethodException e) { return null; } } private static class ThrowRunnable implements Runnable { private final Throwable mThrowable; public ThrowRunnable(Throwable t) { mThrowable = t; } @Override public void run() { ExceptionUtil.sneakyThrow(mThrowable); } } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/dumpapp/plugins/FilesDumperPlugin.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.dumpapp.plugins; import android.content.Context; import android.os.Environment; import com.facebook.stetho.common.Util; import com.facebook.stetho.dumpapp.ArgsHelper; import com.facebook.stetho.dumpapp.DumpException; import com.facebook.stetho.dumpapp.DumpUsageException; import com.facebook.stetho.dumpapp.DumperContext; import com.facebook.stetho.dumpapp.DumperPlugin; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintStream; import java.util.ArrayList; import java.util.Iterator; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; public class FilesDumperPlugin implements DumperPlugin { private static final String NAME = "files"; private final Context mContext; public FilesDumperPlugin(Context context) { mContext = context; } @Override public String getName() { return NAME; } @Override public void dump(DumperContext dumpContext) throws DumpException { Iterator args = dumpContext.getArgsAsList().iterator(); String command = ArgsHelper.nextOptionalArg(args, ""); if ("ls".equals(command)) { doLs(dumpContext.getStdout()); } else if ("tree".equals(command)) { doTree(dumpContext.getStdout()); } else if ("download".equals(command)) { doDownload(dumpContext.getStdout(), args); } else { doUsage(dumpContext.getStdout()); if (!"".equals(command)) { throw new DumpUsageException("Unknown command: " + command); } } } private void doLs(PrintStream writer) throws DumpUsageException { File baseDir = getBaseDir(mContext); if (baseDir.isDirectory()) { printDirectoryText(baseDir, "", writer); } } private void doTree(PrintStream writer) throws DumpUsageException { File baseDir = getBaseDir(mContext); printDirectoryVisual(baseDir, 0, writer); } private static File getBaseDir(Context context) { // getFilesDir() yields /data/data//files, we want the base package dir. return context.getFilesDir().getParentFile(); } private static void printDirectoryText(File dir, String path, PrintStream writer) { File[] listFiles = dir.listFiles(); for (int i = 0; i < listFiles.length; ++i) { File file = listFiles[i]; if (file.isDirectory()) { printDirectoryText(file, path + file.getName() + "/", writer); } else { writer.println(path + file.getName()); } } } private static void printDirectoryVisual(File dir, int depth, PrintStream writer) { File[] listFiles = dir.listFiles(); for (int i = 0; i < listFiles.length; ++i) { printHeaderVisual(depth, writer); File file = listFiles[i]; writer.print("+---"); writer.print(file.getName()); writer.println(); if (file.isDirectory()) { printDirectoryVisual(file, depth + 1, writer); } } } private static void printHeaderVisual(int depth, PrintStream writer) { for (int i = 0; i < depth; ++i) { writer.print("| "); } } private void doDownload(PrintStream writer, Iterator remainingArgs) throws DumpUsageException { String outputPath = ArgsHelper.nextArg(remainingArgs, "Must specify output file or '-'"); ArrayList selectedFiles = new ArrayList<>(); while (remainingArgs.hasNext()) { selectedFiles.add(resolvePossibleAppStoragePath(mContext, remainingArgs.next())); } try { OutputStream outputStream; if ("-".equals(outputPath)) { outputStream = writer; } else { outputStream = new FileOutputStream(resolvePossibleSdcardPath(outputPath)); } ZipOutputStream output = new ZipOutputStream(new BufferedOutputStream(outputStream)); boolean success = false; try { byte[] buf = new byte[2048]; if (selectedFiles.size() > 0) { addFiles(output, buf, selectedFiles.toArray(new File[selectedFiles.size()])); } else { addFiles(output, buf, getBaseDir(mContext).listFiles()); } success = true; } finally { try { output.close(); } catch (IOException e) { Util.close(outputStream, !success); if (success) { throw e; } } } } catch (IOException e) { throw new RuntimeException(e); } } private void addFiles(ZipOutputStream output, byte[] buf, File[] files) throws IOException { for (File file : files) { if (file.isDirectory()) { addFiles(output, buf, file.listFiles()); } else { output.putNextEntry( new ZipEntry( relativizePath( getBaseDir(mContext).getParentFile(), file))); FileInputStream input = new FileInputStream(file); try { copy(input, output, buf); } finally { input.close(); } } } } private static void copy(InputStream in, OutputStream out, byte[] buf) throws IOException { int n; while ((n = in.read(buf)) >= 0) { out.write(buf, 0, n); } } // Disclaimer: stupid implementation :) private static String relativizePath(File base, File path) { String baseStr = base.getAbsolutePath(); String pathStr = path.getAbsolutePath(); if (pathStr.startsWith(baseStr)) { return pathStr.substring(baseStr.length() + 1); } else { return pathStr; } } private static File resolvePossibleAppStoragePath(Context context, String path) { if (path.startsWith("/")) { return new File(path); } else { return new File(getBaseDir(context), path); } } private static File resolvePossibleSdcardPath(String path) { if (path.startsWith("/")) { return new File(path); } else { return new File(Environment.getExternalStorageDirectory(), path); } } private void doUsage(PrintStream writer) { final String cmdName = "dumpapp " + NAME; String usagePrefix = "Usage: " + cmdName + " "; String blankPrefix = " " + cmdName + " "; writer.println(usagePrefix + " [command-options]"); writer.println(blankPrefix + "ls"); writer.println(blankPrefix + "tree"); writer.println(blankPrefix + "download [...]"); writer.println(); writer.println(cmdName + " ls: List files similar to the ls command"); writer.println(); writer.println(cmdName + " tree: List files similar to the tree command"); writer.println(); writer.println(cmdName + " download: Fetch internal application storage"); writer.println(" : Output location or '-' for stdout"); writer.println(" : Fetch only those paths named (directories fetch recursively)"); } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/dumpapp/plugins/HprofDumperPlugin.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.dumpapp.plugins; import android.content.Context; import android.os.Debug; import com.facebook.stetho.common.Util; import com.facebook.stetho.dumpapp.DumpException; import com.facebook.stetho.dumpapp.DumpUsageException; import com.facebook.stetho.dumpapp.DumperContext; import com.facebook.stetho.dumpapp.DumperPlugin; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintStream; import java.util.Iterator; /** * Generates an hprof on the sdcard and returns to the client the absolute path to the file. *

* For convenient usage, see: {@code scripts/hprof_dump.sh} */ public class HprofDumperPlugin implements DumperPlugin { private static final String NAME = "hprof"; private final Context mContext; public HprofDumperPlugin(Context context) { mContext = context; } @Override public String getName() { return NAME; } @Override public void dump(DumperContext dumpContext) throws DumpException { final PrintStream output = dumpContext.getStdout(); Iterator argsIter = dumpContext.getArgsAsList().iterator(); String outputPath = argsIter.hasNext() ? argsIter.next() : null; if (outputPath == null) { usage(output); } else { if ("-".equals(outputPath)) { handlePipeOutput(output); } else { File outputFile = new File(outputPath); if (!outputFile.isAbsolute()) { outputFile = mContext.getFileStreamPath(outputPath); } writeHprof(outputFile); output.println("Wrote to " + outputFile); } } } private void handlePipeOutput(OutputStream output) throws DumpException { File hprofFile = mContext.getFileStreamPath("hprof-dump.hprof"); try { writeHprof(hprofFile); try { InputStream input = new FileInputStream(hprofFile); try { Util.copy(input, output, new byte[2048]); } finally { input.close(); } } catch (IOException e) { throw new DumpException("Failure copying " + hprofFile + " to dumper output"); } } finally { if (hprofFile.exists()) { hprofFile.delete(); } } } private void writeHprof(File outputPath) throws DumpException { try { // Test that we can write here. dumpHprofData appears to hang if it cannot write // to the target location on ART. truncateAndDeleteFile(outputPath); Debug.dumpHprofData(outputPath.getAbsolutePath()); } catch (IOException e) { throw new DumpException("Failure writing to " + outputPath + ": " + e.getMessage()); } } private static void truncateAndDeleteFile(File file) throws IOException { FileOutputStream out = new FileOutputStream(file); out.close(); if (!file.delete()) { throw new IOException("Failed to delete " + file); } } private void usage(PrintStream output) throws DumpUsageException { output.println("Usage: dumpapp hprof [ path ]"); output.println("Dump HPROF memory usage data from the running application."); output.println(); output.println("Where path can be any of:"); output.println(" - Output directly to stdout"); output.println(" Full path to a writable file on the device"); output.println(" Relative filename that will be stored in the app internal storage"); throw new DumpUsageException("Missing path"); } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/dumpapp/plugins/SharedPreferencesDumperPlugin.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.dumpapp.plugins; import android.annotation.SuppressLint; import android.content.Context; import android.content.SharedPreferences; import android.text.TextUtils; import com.facebook.stetho.dumpapp.DumpUsageException; import com.facebook.stetho.dumpapp.DumperContext; import com.facebook.stetho.dumpapp.DumperPlugin; import com.facebook.stetho.inspector.domstorage.SharedPreferencesHelper; import java.io.File; import java.io.PrintStream; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import javax.annotation.Nonnull; import javax.annotation.Nullable; public class SharedPreferencesDumperPlugin implements DumperPlugin { private static final String XML_SUFFIX = ".xml"; private static final String NAME = "prefs"; private final Context mAppContext; public SharedPreferencesDumperPlugin(Context context) { mAppContext = context.getApplicationContext(); } @Override public String getName() { return NAME; } @Override public void dump(DumperContext dumpContext) throws DumpUsageException { PrintStream writer = dumpContext.getStdout(); List args = dumpContext.getArgsAsList(); String commandName = args.isEmpty() ? "" : args.remove(0); if (commandName.equals("print")) { doPrint(writer, args); } else if (commandName.equals("write")) { doWrite(args); } else { doUsage(writer); } } /** * Executes command to update one value in the shared preferences */ // We explicitly want commit() so that the dumper blocks while the write occurs. @SuppressLint("CommitPrefEdits") private void doWrite(List args) throws DumpUsageException { String usagePrefix = "Usage: prefs write , where type is one of: "; Iterator argsIter = args.iterator(); String path = nextArg(argsIter, "Expected "); String key = nextArg(argsIter, "Expected "); String typeName = nextArg(argsIter, "Expected "); Type type = Type.of(typeName); if (type == null) { throw new DumpUsageException( Type.appendNamesList(new StringBuilder(usagePrefix), ", ").toString()); } SharedPreferences sharedPreferences = getSharedPreferences(path); SharedPreferences.Editor editor = sharedPreferences.edit(); switch (type) { case BOOLEAN: editor.putBoolean(key, Boolean.valueOf(nextArgValue(argsIter))); break; case INT: editor.putInt(key, Integer.valueOf(nextArgValue(argsIter))); break; case LONG: editor.putLong(key, Long.valueOf(nextArgValue(argsIter))); break; case FLOAT: editor.putFloat(key, Float.valueOf(nextArgValue(argsIter))); break; case STRING: editor.putString(key, nextArgValue(argsIter)); break; case SET: putStringSet(editor, key, argsIter); break; } editor.commit(); } @Nonnull private static String nextArg(Iterator iter, String messageIfNotPresent) throws DumpUsageException { if (!iter.hasNext()) { throw new DumpUsageException(messageIfNotPresent); } return iter.next(); } @Nonnull private static String nextArgValue(Iterator iter) throws DumpUsageException { return nextArg(iter, "Expected "); } private static void putStringSet( SharedPreferences.Editor editor, String key, Iterator remainingArgs) { HashSet set = new HashSet(); while (remainingArgs.hasNext()) { set.add(remainingArgs.next()); } editor.putStringSet(key, set); } /** * Execute command to print all keys and values stored in the shared preferences which match * the optional given prefix */ private void doPrint(PrintStream writer, List args) { String rootPath = mAppContext.getApplicationInfo().dataDir + "/shared_prefs"; String offsetPrefix = args.isEmpty() ? "" : args.get(0); String keyPrefix = (args.size() > 1) ? args.get(1) : ""; printRecursive(writer, rootPath, "", offsetPrefix, keyPrefix); } private void printRecursive( PrintStream writer, String rootPath, String offsetPath, String pathPrefix, String keyPrefix) { File file = new File(rootPath, offsetPath); if (file.isFile()) { if (offsetPath.endsWith(XML_SUFFIX)) { int suffixLength = XML_SUFFIX.length(); String prefsName = offsetPath.substring(0, offsetPath.length() - suffixLength); printFile(writer, prefsName, keyPrefix); } } else if (file.isDirectory()) { String[] children = file.list(); if (children != null) { for (int i = 0; i < children.length; i++) { String childOffsetPath = TextUtils.isEmpty(offsetPath) ? children[i] : (offsetPath + File.separator + children[i]); if (childOffsetPath.startsWith(pathPrefix)) { printRecursive(writer, rootPath, childOffsetPath, pathPrefix, keyPrefix); } } } } } private void printFile(PrintStream writer, String prefsName, String keyPrefix) { writer.println(prefsName + ":"); SharedPreferences preferences = getSharedPreferences(prefsName); for (Map.Entry entry : SharedPreferencesHelper.getSharedPreferenceEntriesSorted(preferences)) { if (entry.getKey().startsWith(keyPrefix)) { writer.println(" " + entry.getKey() + " = " + entry.getValue()); } } } private void doUsage(PrintStream writer) { final String cmdName = "dumpapp " + NAME; String usagePrefix = "Usage: " + cmdName + " "; String blankPrefix = " " + cmdName + " "; writer.println(usagePrefix + " [command-options]"); writer.println(usagePrefix + "print [pathPrefix [keyPrefix]]"); writer.println( Type.appendNamesList( new StringBuilder(blankPrefix).append("write <"), "|") .append("> ")); writer.println(); writer.println(cmdName + " print: Print all matching values from the shared preferences"); writer.println(); writer.println(cmdName + " write: Writes a value to the shared preferences"); } private SharedPreferences getSharedPreferences(String name) { return mAppContext.getSharedPreferences(name, Context.MODE_MULTI_PROCESS); } private enum Type { BOOLEAN("boolean"), INT("int"), LONG("long"), FLOAT("float"), STRING("string"), SET("set"); private final String name; private Type(String name) { this.name = name; } public static @Nullable Type of(String name) { for (Type type : values()) { if (type.name.equals(name)) { return type; } } return null; } public static StringBuilder appendNamesList(StringBuilder builder, String separator) { boolean isFirst = true; for (Type type : values()) { if (isFirst) { isFirst = false; } else { builder.append(separator); } builder.append(type.name); } return builder; } } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/ChromeDevtoolsServer.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector; import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.Map; import android.util.Log; import com.facebook.stetho.common.LogRedirector; import com.facebook.stetho.common.Util; import com.facebook.stetho.inspector.jsonrpc.JsonRpcException; import com.facebook.stetho.inspector.jsonrpc.JsonRpcPeer; import com.facebook.stetho.inspector.jsonrpc.PendingRequest; import com.facebook.stetho.inspector.jsonrpc.protocol.JsonRpcError; import com.facebook.stetho.inspector.jsonrpc.protocol.JsonRpcRequest; import com.facebook.stetho.inspector.jsonrpc.protocol.JsonRpcResponse; import com.facebook.stetho.inspector.protocol.ChromeDevtoolsDomain; import com.facebook.stetho.json.ObjectMapper; import com.facebook.stetho.websocket.CloseCodes; import com.facebook.stetho.websocket.SimpleEndpoint; import com.facebook.stetho.websocket.SimpleSession; import org.json.JSONException; import org.json.JSONObject; /** * Implements a limited version of the Chrome Debugger WebSocket protocol (using JSON-RPC 2.0). * The most up-to-date documentation can be found in the Blink source code: * protocol.json */ public class ChromeDevtoolsServer implements SimpleEndpoint { private static final String TAG = "ChromeDevtoolsServer"; public static final String PATH = "/inspector"; private final ObjectMapper mObjectMapper; private final MethodDispatcher mMethodDispatcher; private final Map mPeers = Collections.synchronizedMap( new HashMap()); public ChromeDevtoolsServer(Iterable domainModules) { mObjectMapper = new ObjectMapper(); mMethodDispatcher = new MethodDispatcher(mObjectMapper, domainModules); } @Override public void onOpen(SimpleSession session) { LogRedirector.d(TAG, "onOpen"); mPeers.put(session, new JsonRpcPeer(mObjectMapper, session)); } @Override public void onClose(SimpleSession session, int code, String reasonPhrase) { LogRedirector.d(TAG, "onClose: reason=" + code + " " + reasonPhrase); JsonRpcPeer peer = mPeers.remove(session); if (peer != null) { peer.invokeDisconnectReceivers(); } } @Override public void onMessage(SimpleSession session, byte[] message, int messageLen) { LogRedirector.d(TAG, "Ignoring binary message of length " + messageLen); } @Override public void onMessage(SimpleSession session, String message) { if (LogRedirector.isLoggable(TAG, Log.VERBOSE)) { LogRedirector.v(TAG, "onMessage: message=" + message); } try { JsonRpcPeer peer = mPeers.get(session); Util.throwIfNull(peer); handleRemoteMessage(peer, message); } catch (IOException e) { if (LogRedirector.isLoggable(TAG, Log.VERBOSE)) { LogRedirector.v(TAG, "Unexpected I/O exception processing message: " + e); } closeSafely(session, CloseCodes.UNEXPECTED_CONDITION, e.getClass().getSimpleName()); } catch (MessageHandlingException e) { LogRedirector.i(TAG, "Message could not be processed by implementation: " + e); closeSafely(session, CloseCodes.UNEXPECTED_CONDITION, e.getClass().getSimpleName()); } catch (JSONException e) { LogRedirector.v(TAG, "Unexpected JSON exception processing message", e); closeSafely(session, CloseCodes.UNEXPECTED_CONDITION, e.getClass().getSimpleName()); } } private void closeSafely(SimpleSession session, int code, String reasonPhrase) { session.close(code, reasonPhrase); } private void handleRemoteMessage(JsonRpcPeer peer, String message) throws IOException, MessageHandlingException, JSONException { // Parse as a generic JSONObject first since we don't know if this is a request or response. JSONObject messageNode = new JSONObject(message); if (messageNode.has("method")) { handleRemoteRequest(peer, messageNode); } else if (messageNode.has("result")) { handleRemoteResponse(peer, messageNode); } else { throw new MessageHandlingException("Improper JSON-RPC message: " + message); } } private void handleRemoteRequest(JsonRpcPeer peer, JSONObject requestNode) throws MessageHandlingException { JsonRpcRequest request; request = mObjectMapper.convertValue( requestNode, JsonRpcRequest.class); JSONObject result = null; JSONObject error = null; try { result = mMethodDispatcher.dispatch(peer, request.method, request.params); } catch (JsonRpcException e) { logDispatchException(e); error = mObjectMapper.convertValue(e.getErrorMessage(), JSONObject.class); } if (request.id != null) { JsonRpcResponse response = new JsonRpcResponse(); response.id = request.id; response.result = result; response.error = error; JSONObject jsonObject = mObjectMapper.convertValue(response, JSONObject.class); String responseString; try { responseString = jsonObject.toString(); } catch (OutOfMemoryError e) { // JSONStringer can cause an OOM when the Json to handle is too big. response.result = null; response.error = mObjectMapper.convertValue(e.getMessage(), JSONObject.class); jsonObject = mObjectMapper.convertValue(response, JSONObject.class); responseString = jsonObject.toString(); } peer.getWebSocket().sendText(responseString); } } private static void logDispatchException(JsonRpcException e) { JsonRpcError errorMessage = e.getErrorMessage(); switch (errorMessage.code) { case METHOD_NOT_FOUND: LogRedirector.d(TAG, "Method not implemented: " + errorMessage.message); break; default: LogRedirector.w(TAG, "Error processing remote message", e); } } private void handleRemoteResponse(JsonRpcPeer peer, JSONObject responseNode) throws MismatchedResponseException { JsonRpcResponse response = mObjectMapper.convertValue( responseNode, JsonRpcResponse.class); PendingRequest pendingRequest = peer.getAndRemovePendingRequest(response.id); if (pendingRequest == null) { throw new MismatchedResponseException(response.id); } if (pendingRequest.callback != null) { pendingRequest.callback.onResponse(peer, response); } } @Override public void onError(SimpleSession session, Throwable ex) { LogRedirector.e(TAG, "onError: ex=" + ex.toString()); } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/ChromeDiscoveryHandler.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ 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_PAGE_LIST1 = "/json/list"; 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 = "@cfede9db1d154de0468cb0538479f34c0755a0f4"; 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.3"; 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_PAGE_LIST1), 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) || PATH_PAGE_LIST1.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("inspector.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()); } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/DevtoolsSocketHandler.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector; import android.content.Context; import android.net.LocalSocket; import com.facebook.stetho.inspector.protocol.ChromeDevtoolsDomain; import com.facebook.stetho.server.SecureSocketHandler; import com.facebook.stetho.server.SocketLike; import com.facebook.stetho.server.SocketLikeHandler; import com.facebook.stetho.server.http.ExactPathMatcher; import com.facebook.stetho.server.http.HandlerRegistry; import com.facebook.stetho.server.http.LightHttpServer; import com.facebook.stetho.websocket.WebSocketHandler; import java.io.IOException; public class DevtoolsSocketHandler implements SocketLikeHandler { private final Context mContext; private final Iterable mModules; private final LightHttpServer mServer; public DevtoolsSocketHandler(Context context, Iterable modules) { mContext = context; mModules = modules; mServer = createServer(); } private LightHttpServer createServer() { HandlerRegistry registry = new HandlerRegistry(); ChromeDiscoveryHandler discoveryHandler = new ChromeDiscoveryHandler( mContext, ChromeDevtoolsServer.PATH); discoveryHandler.register(registry); registry.register( new ExactPathMatcher(ChromeDevtoolsServer.PATH), new WebSocketHandler(new ChromeDevtoolsServer(mModules))); return new LightHttpServer(registry); } @Override public void onAccepted(SocketLike socket) throws IOException { mServer.serve(socket); } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/MessageHandlingException.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector; public class MessageHandlingException extends Exception { public MessageHandlingException(Throwable cause) { super(cause); } public MessageHandlingException(String message) { super(message); } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/MethodDispatcher.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector; import javax.annotation.Nullable; import javax.annotation.concurrent.GuardedBy; import javax.annotation.concurrent.ThreadSafe; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Collections; import java.util.HashMap; import java.util.Map; import com.facebook.stetho.common.ExceptionUtil; import com.facebook.stetho.common.Util; import com.facebook.stetho.inspector.jsonrpc.JsonRpcException; import com.facebook.stetho.inspector.jsonrpc.JsonRpcPeer; import com.facebook.stetho.inspector.jsonrpc.JsonRpcResult; import com.facebook.stetho.inspector.jsonrpc.protocol.EmptyResult; import com.facebook.stetho.inspector.jsonrpc.protocol.JsonRpcError; import com.facebook.stetho.inspector.protocol.ChromeDevtoolsDomain; import com.facebook.stetho.inspector.protocol.ChromeDevtoolsMethod; import com.facebook.stetho.json.ObjectMapper; import org.json.JSONException; import org.json.JSONObject; @ThreadSafe public class MethodDispatcher { @GuardedBy("this") private Map mMethods; private final ObjectMapper mObjectMapper; private final Iterable mDomainHandlers; public MethodDispatcher( ObjectMapper objectMapper, Iterable domainHandlers) { mObjectMapper = objectMapper; mDomainHandlers = domainHandlers; } private synchronized MethodDispatchHelper findMethodDispatcher(String methodName) { if (mMethods == null) { mMethods = buildDispatchTable(mObjectMapper, mDomainHandlers); } return mMethods.get(methodName); } public JSONObject dispatch(JsonRpcPeer peer, String methodName, @Nullable JSONObject params) throws JsonRpcException { MethodDispatchHelper dispatchHelper = findMethodDispatcher(methodName); if (dispatchHelper == null) { throw new JsonRpcException(new JsonRpcError(JsonRpcError.ErrorCode.METHOD_NOT_FOUND, "Not implemented: " + methodName, null /* data */)); } try { return dispatchHelper.invoke(peer, params); } catch (InvocationTargetException e) { Throwable cause = e.getCause(); ExceptionUtil.propagateIfInstanceOf(cause, JsonRpcException.class); throw ExceptionUtil.propagate(cause); } catch (IllegalAccessException e) { throw new RuntimeException(e); } catch (JSONException e) { throw new JsonRpcException(new JsonRpcError(JsonRpcError.ErrorCode.INTERNAL_ERROR, e.toString(), null /* data */)); } } private static class MethodDispatchHelper { private final ObjectMapper mObjectMapper; private final ChromeDevtoolsDomain mInstance; private final Method mMethod; public MethodDispatchHelper(ObjectMapper objectMapper, ChromeDevtoolsDomain instance, Method method) { mObjectMapper = objectMapper; mInstance = instance; mMethod = method; } public JSONObject invoke(JsonRpcPeer peer, @Nullable JSONObject params) throws InvocationTargetException, IllegalAccessException, JSONException, JsonRpcException { Object internalResult = mMethod.invoke(mInstance, peer, params); if (internalResult == null || internalResult instanceof EmptyResult) { return new JSONObject(); } else { JsonRpcResult convertableResult = (JsonRpcResult)internalResult; return mObjectMapper.convertValue(convertableResult, JSONObject.class); } } } private static Map buildDispatchTable( ObjectMapper objectMapper, Iterable domainHandlers) { Util.throwIfNull(objectMapper); HashMap methods = new HashMap(); for (ChromeDevtoolsDomain domainHandler : Util.throwIfNull(domainHandlers)) { Class handlerClass = domainHandler.getClass(); String domainName = handlerClass.getSimpleName(); for (Method method : handlerClass.getMethods()) { if (isDevtoolsMethod(method)) { MethodDispatchHelper dispatchHelper = new MethodDispatchHelper( objectMapper, domainHandler, method); methods.put(domainName + "." + method.getName(), dispatchHelper); } } } return Collections.unmodifiableMap(methods); } /** * Determines if the method is a {@link ChromeDevtoolsMethod}, and validates accordingly * if it is. * * @throws IllegalArgumentException Thrown if it is a {@link ChromeDevtoolsMethod} but * it otherwise fails to satisfy requirements. */ private static boolean isDevtoolsMethod(Method method) throws IllegalArgumentException { if (!method.isAnnotationPresent(ChromeDevtoolsMethod.class)) { return false; } else { Class args[] = method.getParameterTypes(); String methodName = method.getDeclaringClass().getSimpleName() + "." + method.getName(); Util.throwIfNot(args.length == 2, "%s: expected 2 args, got %s", methodName, args.length); Util.throwIfNot(args[0].equals(JsonRpcPeer.class), "%s: expected 1st arg of JsonRpcPeer, got %s", methodName, args[0].getName()); Util.throwIfNot(args[1].equals(JSONObject.class), "%s: expected 2nd arg of JSONObject, got %s", methodName, args[1].getName()); Class returnType = method.getReturnType(); if (!returnType.equals(void.class)) { Util.throwIfNot(JsonRpcResult.class.isAssignableFrom(returnType), "%s: expected JsonRpcResult return type, got %s", methodName, returnType.getName()); } return true; } } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/MismatchedResponseException.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector; public class MismatchedResponseException extends MessageHandlingException { public long mRequestId; public MismatchedResponseException(long requestId) { super("Response for request id " + requestId + ", but no such request is pending"); mRequestId = requestId; } public long getRequestId() { return mRequestId; } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/console/CLog.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.console; import com.facebook.stetho.common.LogRedirector; import com.facebook.stetho.inspector.helper.ChromePeerManager; import com.facebook.stetho.inspector.protocol.module.Console; import javax.annotation.Nonnull; /** * Utility for reporting an event to the console */ public class CLog { private static final String TAG = "CLog"; public static void writeToConsole( ChromePeerManager chromePeerManager, Console.MessageLevel logLevel, Console.MessageSource messageSource, String messageText) { // Send to logcat to increase the chances that a developer will notice :) LogRedirector.d(TAG, messageText); Console.ConsoleMessage message = new Console.ConsoleMessage(); message.source = messageSource; message.level = logLevel; message.text = messageText; Console.MessageAddedRequest messageAddedRequest = new Console.MessageAddedRequest(); messageAddedRequest.message = message; chromePeerManager.sendNotificationToPeers("Console.messageAdded", messageAddedRequest); } public static void writeToConsole( Console.MessageLevel logLevel, Console.MessageSource messageSource, String messageText ) { ConsolePeerManager peerManager = ConsolePeerManager.getInstanceOrNull(); if (peerManager == null) { return; } writeToConsole(peerManager, logLevel, messageSource, messageText); } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/console/ConsolePeerManager.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.console; import com.facebook.stetho.inspector.helper.ChromePeerManager; import javax.annotation.Nullable; public class ConsolePeerManager extends ChromePeerManager { private static ConsolePeerManager sInstance; private ConsolePeerManager() { super(); } @Nullable public static synchronized ConsolePeerManager getInstanceOrNull() { return sInstance; } public static synchronized ConsolePeerManager getOrCreateInstance() { if (sInstance == null) { sInstance = new ConsolePeerManager(); } return sInstance; } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/console/RuntimeRepl.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.console; public interface RuntimeRepl { Object evaluate(String expression) throws Throwable; } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/console/RuntimeReplFactory.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.console; /** * Allows callers to specify their own Console tab REPL for the DevTools UI. This is part of * early support for a possible optionally included default implementation for Android. *

* A new {@link RuntimeRepl} instances is created for each unique peer such that memory * can be garbage collected when the peer disconnects. *

* This is provided as part of an experimental API. Depend on it at your own risk... */ public interface RuntimeReplFactory { RuntimeRepl newInstance(); } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/database/ContentProviderDatabaseDriver.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.database; import android.content.ContentResolver; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteException; import com.facebook.stetho.inspector.protocol.module.Database; import com.facebook.stetho.inspector.protocol.module.DatabaseDescriptor; import com.facebook.stetho.inspector.protocol.module.DatabaseDriver2; import java.util.ArrayList; import java.util.Collections; import java.util.List; import javax.annotation.concurrent.ThreadSafe; @ThreadSafe public class ContentProviderDatabaseDriver extends DatabaseDriver2 { private final static String sDatabaseName = "content-providers"; private final ContentProviderSchema[] mContentProviderSchemas; private List mTableNames; public ContentProviderDatabaseDriver( Context context, ContentProviderSchema... contentProviderSchemas) { super(context); mContentProviderSchemas = contentProviderSchemas; } @Override public List getDatabaseNames() { return Collections.singletonList(new ContentProviderDatabaseDescriptor()); } @Override public List getTableNames(ContentProviderDatabaseDescriptor databaseDesc) { if (mTableNames == null) { mTableNames = new ArrayList<>(); for (ContentProviderSchema schema : mContentProviderSchemas) { mTableNames.add(schema.getTableName()); } } return mTableNames; } @Override public Database.ExecuteSQLResponse executeSQL( ContentProviderDatabaseDescriptor databaseDesc, String query, ExecuteResultHandler handler) throws SQLiteException { // resolve table name from query String tableName = fetchTableName(query); // find the right ContentProviderSchema int index = mTableNames.indexOf(tableName); ContentProviderSchema contentProviderSchema = mContentProviderSchemas[index]; // execute the query ContentResolver contentResolver = mContext.getContentResolver(); Cursor cursor = contentResolver.query( contentProviderSchema.getUri(), contentProviderSchema.getProjection(), null, null, null); try { return handler.handleSelect(cursor); } finally { cursor.close(); } } /** * Fetch the table name from query */ private String fetchTableName(String query) { for (String tableName : mTableNames) { if (query.contains(tableName)) { return tableName; } } return ""; } static class ContentProviderDatabaseDescriptor implements DatabaseDescriptor { public ContentProviderDatabaseDescriptor() { } @Override public String name() { // Hmm, this probably should be each unique URI or authority instead of treating all // content provider instances as one. return sDatabaseName; } } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/database/ContentProviderSchema.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.database; import android.net.Uri; public class ContentProviderSchema { private final String mTableName; private final Uri mUri; private final String[] mProjection; private ContentProviderSchema(Builder builder) { mTableName = builder.mTable.mTableName; mUri = builder.mTable.mUri; mProjection = builder.mTable.mProjection; } public String getTableName() { return mTableName; } public Uri getUri() { return mUri; } public String[] getProjection() { return mProjection; } public static class Builder { private Table mTable; public Builder table(Table table) { mTable = table; return this; } public ContentProviderSchema build() { return new ContentProviderSchema(this); } } public static class Table { private Uri mUri; private String[] mProjection; private String mTableName; private Table(Builder builder) { mUri = builder.mUri; mProjection = builder.mProjection; mTableName = builder.mTableName; if (mTableName == null) { mTableName = mUri.getLastPathSegment(); } } public static class Builder { private Uri mUri; private String[] mProjection; private String mTableName; public Builder uri(Uri contentUri) { mUri = contentUri; return this; } // optional public Builder projection(String[] columns) { mProjection = columns; return this; } // optional, if not set, last segment of URI will be used as table name public Builder name(String tableName) { mTableName = tableName; return this; } public Table build() { return new Table(this); } } } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/database/DatabaseConnectionProvider.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.database; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteException; import java.io.File; /** * Provides a {@link SQLiteDatabase} connection for the specified database. For use with * {@link SqliteDatabaseDriver}. */ public interface DatabaseConnectionProvider { /** * @param databaseFile Full path to the database file. * @return a connection for the specified databaseName. * @throws SQLiteException if there is an error opening the specified database */ SQLiteDatabase openDatabase(File databaseFile) throws SQLiteException; } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/database/DatabaseDriver2Adapter.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.database; import android.database.sqlite.SQLiteException; import com.facebook.stetho.inspector.protocol.module.Database; import com.facebook.stetho.inspector.protocol.module.DatabaseDescriptor; import com.facebook.stetho.inspector.protocol.module.DatabaseDriver2; import java.util.ArrayList; import java.util.List; /** * @deprecated Use {@link DatabaseDriver2} directly. This is provided only for legacy * drivers to be adapted internally within Stetho. */ @Deprecated public class DatabaseDriver2Adapter extends DatabaseDriver2 { private final Database.DatabaseDriver mLegacy; public DatabaseDriver2Adapter(Database.DatabaseDriver legacy) { super(legacy.getContext()); mLegacy = legacy; } @Override public List getDatabaseNames() { List names = mLegacy.getDatabaseNames(); List descriptors = new ArrayList<>(names.size()); for (Object name : names) { descriptors.add(new StringDatabaseDescriptor(name.toString())); } return descriptors; } @SuppressWarnings("unchecked") public List getTableNames(StringDatabaseDescriptor database) { return mLegacy.getTableNames(database.name); } @SuppressWarnings("unchecked") public Database.ExecuteSQLResponse executeSQL( StringDatabaseDescriptor database, String query, ExecuteResultHandler handler) throws SQLiteException { return mLegacy.executeSQL(database.name, query, handler); } static class StringDatabaseDescriptor implements DatabaseDescriptor { public final String name; public StringDatabaseDescriptor(String name) { this.name = name; } @Override public String name() { return name; } } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/database/DatabaseFilesProvider.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.database; import java.io.File; import java.util.List; /** * Provides a {@link List} of database files. */ public interface DatabaseFilesProvider { /** * Returns a {@link List} of database files. */ List getDatabaseFiles(); } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/database/DefaultDatabaseConnectionProvider.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.database; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteException; import com.facebook.stetho.inspector.database.SQLiteDatabaseCompat.SQLiteOpenOptions; import java.io.File; /** * Opens the requested database using * {@link SQLiteDatabase#openDatabase(String, SQLiteDatabase.CursorFactory, int)} directly. * *

It is intended that this class be subclassed to enable/disable features via * {@link #determineOpenOptions(File)}

*/ public class DefaultDatabaseConnectionProvider implements DatabaseConnectionProvider { public DefaultDatabaseConnectionProvider() { } @Override public SQLiteDatabase openDatabase(File databaseFile) throws SQLiteException { return performOpen( databaseFile, determineOpenOptions(databaseFile)); } /** * Subclassing this function is intended to provide custom open behaviour on a per-file basis. */ protected @SQLiteOpenOptions int determineOpenOptions(File databaseFile) { @SQLiteOpenOptions int flags = 0; // Try to guess if we should be using write-ahead logging. If this heuristic fails // developers are expected to subclass this provider and explicitly assert the connection. File walFile = new File(databaseFile.getParent(), databaseFile.getName() + "-wal"); if (walFile.exists()) { flags |= SQLiteDatabaseCompat.ENABLE_WRITE_AHEAD_LOGGING; } return flags; } /** * Perform the open per the options provided in {@link #determineOpenOptions(File)}. * Subclassing is supported however this typically indicates a missing feature of some kind * in {@link SQLiteDatabaseCompat} that should be patched in Stetho itself. */ protected SQLiteDatabase performOpen(File databaseFile, @SQLiteOpenOptions int options) { int flags = SQLiteDatabase.OPEN_READWRITE; SQLiteDatabaseCompat compatInstance = SQLiteDatabaseCompat.getInstance(); flags |= compatInstance.provideOpenFlags(options); SQLiteDatabase db = SQLiteDatabase.openDatabase( databaseFile.getAbsolutePath(), null /* cursorFactory */, flags); compatInstance.enableFeatures(options, db); return db; } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/database/DefaultDatabaseFilesProvider.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.database; import android.content.Context; import java.io.File; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * Provides the results of {@link Context#databaseList()} for {@link SqliteDatabaseDriver}. */ public final class DefaultDatabaseFilesProvider implements DatabaseFilesProvider { private final Context mContext; public DefaultDatabaseFilesProvider(Context context) { mContext = context; } @Override public List getDatabaseFiles() { List databaseFiles = new ArrayList(); for (String databaseName : mContext.databaseList()) { databaseFiles.add(mContext.getDatabasePath(databaseName)); } return databaseFiles; } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/database/SQLiteDatabaseCompat.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.database; import android.annotation.TargetApi; import android.database.sqlite.SQLiteDatabase; import android.os.Build; import androidx.annotation.IntDef; /** * Compatibility layer which supports opening databases with WAL and foreign key support * where supported. * *

For simplicity of implementation, all options are ignored prior to Honeycomb.

*/ public abstract class SQLiteDatabaseCompat { public static final int ENABLE_WRITE_AHEAD_LOGGING = 0x1; public static final int ENABLE_FOREIGN_KEY_CONSTRAINTS = 0x2; @IntDef( value = { ENABLE_WRITE_AHEAD_LOGGING, ENABLE_FOREIGN_KEY_CONSTRAINTS }, flag = true) public @interface SQLiteOpenOptions {} private static final SQLiteDatabaseCompat sInstance; static { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { sInstance = new JellyBeanAndBeyondImpl(); } else { sInstance = new IceCreamSandwichImpl(); } } public static SQLiteDatabaseCompat getInstance() { return sInstance; } public abstract int provideOpenFlags(@SQLiteOpenOptions int openOptions); public abstract void enableFeatures(@SQLiteOpenOptions int openOptions, SQLiteDatabase db); @TargetApi(Build.VERSION_CODES.JELLY_BEAN) private static class JellyBeanAndBeyondImpl extends SQLiteDatabaseCompat { @Override public int provideOpenFlags(@SQLiteOpenOptions int openOptions) { int openFlags = 0; if ((openOptions & ENABLE_WRITE_AHEAD_LOGGING) != 0) { openFlags |= SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING; } return openFlags; } @Override public void enableFeatures(@SQLiteOpenOptions int openOptions, SQLiteDatabase db) { if ((openOptions & ENABLE_FOREIGN_KEY_CONSTRAINTS) != 0) { db.setForeignKeyConstraintsEnabled(true); } } } @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) private static class IceCreamSandwichImpl extends SQLiteDatabaseCompat { @Override public int provideOpenFlags(@SQLiteOpenOptions int openOptions) { return 0; } @Override public void enableFeatures(@SQLiteOpenOptions int openOptions, SQLiteDatabase db) { if ((openOptions & ENABLE_WRITE_AHEAD_LOGGING) != 0) { db.enableWriteAheadLogging(); } if ((openOptions & ENABLE_FOREIGN_KEY_CONSTRAINTS) != 0) { db.execSQL("PRAGMA foreign_keys = ON"); } } } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/database/SqliteDatabaseDriver.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.database; import android.annotation.TargetApi; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteException; import android.database.sqlite.SQLiteStatement; import com.facebook.stetho.common.Util; import com.facebook.stetho.inspector.protocol.module.Database; import com.facebook.stetho.inspector.protocol.module.DatabaseConstants; import com.facebook.stetho.inspector.protocol.module.DatabaseDescriptor; import com.facebook.stetho.inspector.protocol.module.DatabaseDriver2; import java.io.File; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import javax.annotation.concurrent.ThreadSafe; @ThreadSafe public class SqliteDatabaseDriver extends DatabaseDriver2 { private static final String[] UNINTERESTING_FILENAME_SUFFIXES = new String[]{ "-journal", "-shm", "-uid", "-wal" }; private final DatabaseFilesProvider mDatabaseFilesProvider; private final DatabaseConnectionProvider mDatabaseConnectionProvider; /** * Constructs the object with a {@link DatabaseFilesProvider} that supplies the database files * from {@link Context#databaseList()}. * * @param context the context * @deprecated use {@link SqliteDatabaseDriver#SqliteDatabaseDriver(Context, String, DatabaseFilesProvider, DatabaseConnectionProvider)} */ @Deprecated public SqliteDatabaseDriver(Context context) { this( context, new DefaultDatabaseFilesProvider(context), new DefaultDatabaseConnectionProvider()); } /** * @deprecated use {@link SqliteDatabaseDriver#SqliteDatabaseDriver(Context, String, DatabaseFilesProvider, DatabaseConnectionProvider)} */ @Deprecated public SqliteDatabaseDriver( Context context, DatabaseFilesProvider databaseFilesProvider) { this( context, databaseFilesProvider, new DefaultDatabaseConnectionProvider()); } /** * @param context the context * @param namespace label to apply to the driver when it appears in the UI * @param databaseFilesProvider a database file name provider * @param databaseConnectionProvider a database connection provider */ public SqliteDatabaseDriver( Context context, DatabaseFilesProvider databaseFilesProvider, DatabaseConnectionProvider databaseConnectionProvider) { super(context); mDatabaseFilesProvider = databaseFilesProvider; mDatabaseConnectionProvider = databaseConnectionProvider; } @Override public List getDatabaseNames() { ArrayList databases = new ArrayList<>(); List potentialDatabaseFiles = mDatabaseFilesProvider.getDatabaseFiles(); Collections.sort(potentialDatabaseFiles); Iterable tidiedList = tidyDatabaseList(potentialDatabaseFiles); for (File database : tidiedList) { databases.add(new SqliteDatabaseDescriptor(database)); } return databases; } /** * Attempt to smartly eliminate uninteresting shadow databases such as -journal and -uid. Note * that this only removes the database if it is true that it shadows another database lacking * the uninteresting suffix. * * @param databaseFiles Raw list of database files. * @return Tidied list with shadow databases removed. */ // @VisibleForTesting static List tidyDatabaseList(List databaseFiles) { Set originalAsSet = new HashSet(databaseFiles); List tidiedList = new ArrayList(); for (File databaseFile : databaseFiles) { String databaseFilename = databaseFile.getPath(); String sansSuffix = removeSuffix(databaseFilename, UNINTERESTING_FILENAME_SUFFIXES); if (sansSuffix.equals(databaseFilename) || !originalAsSet.contains(new File(sansSuffix))) { tidiedList.add(databaseFile); } } return tidiedList; } private static String removeSuffix(String str, String[] suffixesToRemove) { for (String suffix : suffixesToRemove) { if (str.endsWith(suffix)) { return str.substring(0, str.length() - suffix.length()); } } return str; } public List getTableNames(SqliteDatabaseDescriptor databaseDesc) throws SQLiteException { SQLiteDatabase database = openDatabase(databaseDesc); try { Cursor cursor = database.rawQuery("SELECT name FROM sqlite_master WHERE type IN (?, ?)", new String[] { "table", "view" }); try { List tableNames = new ArrayList(); while (cursor.moveToNext()) { tableNames.add(cursor.getString(0)); } return tableNames; } finally { cursor.close(); } } finally { database.close(); } } public Database.ExecuteSQLResponse executeSQL( SqliteDatabaseDescriptor databaseDesc, String query, ExecuteResultHandler handler) throws SQLiteException { Util.throwIfNull(query); Util.throwIfNull(handler); SQLiteDatabase database = openDatabase(databaseDesc); try { String firstWordUpperCase = getFirstWord(query).toUpperCase(); switch (firstWordUpperCase) { case "UPDATE": case "DELETE": return executeUpdateDelete(database, query, handler); case "INSERT": return executeInsert(database, query, handler); case "SELECT": case "PRAGMA": case "EXPLAIN": return executeSelect(database, query, handler); default: return executeRawQuery(database, query, handler); } } finally { database.close(); } } private static String getFirstWord(String s) { s = s.trim(); int firstSpace = s.indexOf(' '); return firstSpace >= 0 ? s.substring(0, firstSpace) : s; } @TargetApi(DatabaseConstants.MIN_API_LEVEL) private T executeUpdateDelete( SQLiteDatabase database, String query, ExecuteResultHandler handler) { SQLiteStatement statement = database.compileStatement(query); int count = statement.executeUpdateDelete(); return handler.handleUpdateDelete(count); } private T executeInsert( SQLiteDatabase database, String query, ExecuteResultHandler handler) { SQLiteStatement statement = database.compileStatement(query); long count = statement.executeInsert(); return handler.handleInsert(count); } private T executeSelect( SQLiteDatabase database, String query, ExecuteResultHandler handler) { Cursor cursor = database.rawQuery(query, null); try { return handler.handleSelect(cursor); } finally { cursor.close(); } } private T executeRawQuery( SQLiteDatabase database, String query, ExecuteResultHandler handler) { database.execSQL(query); return handler.handleRawQuery(); } private SQLiteDatabase openDatabase( SqliteDatabaseDescriptor databaseDesc) throws SQLiteException { Util.throwIfNull(databaseDesc); return mDatabaseConnectionProvider.openDatabase(databaseDesc.file); } static class SqliteDatabaseDescriptor implements DatabaseDescriptor { public final File file; public SqliteDatabaseDescriptor(File file) { this.file = file; } @Override public String name() { return file.getName(); } } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/domstorage/DOMStoragePeerManager.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.domstorage; import android.content.Context; import android.content.SharedPreferences; import com.facebook.stetho.common.LogUtil; import com.facebook.stetho.inspector.console.CLog; import com.facebook.stetho.inspector.helper.ChromePeerManager; import com.facebook.stetho.inspector.helper.PeerRegistrationListener; import com.facebook.stetho.inspector.helper.PeersRegisteredListener; import com.facebook.stetho.inspector.protocol.module.Console; import com.facebook.stetho.inspector.protocol.module.DOMStorage; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; public class DOMStoragePeerManager extends ChromePeerManager { private final Context mContext; public DOMStoragePeerManager(Context context) { mContext = context; setListener(mPeerListener); } public void signalItemRemoved(DOMStorage.StorageId storageId, String key) { DOMStorage.DomStorageItemRemovedParams params = new DOMStorage.DomStorageItemRemovedParams(); params.storageId = storageId; params.key = key; sendNotificationToPeers("DOMStorage.domStorageItemRemoved", params); } public void signalItemAdded(DOMStorage.StorageId storageId, String key, String value) { DOMStorage.DomStorageItemAddedParams params = new DOMStorage.DomStorageItemAddedParams(); params.storageId = storageId; params.key = key; params.newValue = value; sendNotificationToPeers("DOMStorage.domStorageItemAdded", params); } public void signalItemUpdated( DOMStorage.StorageId storageId, String key, String oldValue, String newValue) { DOMStorage.DomStorageItemUpdatedParams params = new DOMStorage.DomStorageItemUpdatedParams(); params.storageId = storageId; params.key = key; params.oldValue = oldValue; params.newValue = newValue; sendNotificationToPeers("DOMStorage.domStorageItemUpdated", params); } private final PeerRegistrationListener mPeerListener = new PeersRegisteredListener() { private final List mPrefsListeners = new ArrayList(); @Override protected synchronized void onFirstPeerRegistered() { // TODO: We list the tags in Page.getResourceTree as well and those are the real fixed // tags that will be observed by the peer. We can fix this by making the page frames // dynamically update in response to DOMStorage events. This would also allow us to // add new SharedPreferences tags as we observe them being created by way of // android.os.FileObserver. List tags = SharedPreferencesHelper.getSharedPreferenceTags(mContext); for (String tag : tags) { SharedPreferences prefs = mContext.getSharedPreferences(tag, Context.MODE_PRIVATE); DevToolsSharedPreferencesListener listener = new DevToolsSharedPreferencesListener(prefs, tag); prefs.registerOnSharedPreferenceChangeListener(listener); mPrefsListeners.add(listener); } } @Override protected synchronized void onLastPeerUnregistered() { for (DevToolsSharedPreferencesListener prefsListener : mPrefsListeners) { prefsListener.unregister(); } mPrefsListeners.clear(); } }; private class DevToolsSharedPreferencesListener implements SharedPreferences.OnSharedPreferenceChangeListener { private final SharedPreferences mPrefs; private final DOMStorage.StorageId mStorageId; /** * Maintains a copy of the prefs data structure so that we can invoke * {@code DOMStorage.domStorageItemUpdated}. This method requires that we know the old * value to perform updates. Using {@code domStorageItemRemoved}/{@code Added} causes a UI * glitch where the item is moved to the end of the list, unfortunately. */ private final Map mCopy; public DevToolsSharedPreferencesListener(SharedPreferences prefs, String tag) { mPrefs = prefs; mStorageId = new DOMStorage.StorageId(); mStorageId.securityOrigin = tag; mStorageId.isLocalStorage = true; mCopy = prefsCopy(prefs.getAll()); } public void unregister() { mPrefs.unregisterOnSharedPreferenceChangeListener(this); } @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { Map entries = sharedPreferences.getAll(); boolean existedBefore = mCopy.containsKey(key); boolean existsNow = entries.containsKey(key); Object newValue = existsNow ? entries.get(key) : null; if (existedBefore && existsNow) { signalItemUpdated( mStorageId, key, SharedPreferencesHelper.valueToString(mCopy.get(key)), SharedPreferencesHelper.valueToString(newValue)); mCopy.put(key, newValue); } else if (existedBefore) { signalItemRemoved(mStorageId, key); mCopy.remove(key); } else if (existsNow) { signalItemAdded( mStorageId, key, SharedPreferencesHelper.valueToString(newValue)); mCopy.put(key, newValue); } else { // This can happen due to the async nature of the onSharedPreferenceChanged callback. A // rapid put/remove as two separate commits on a background thread would cause this. LogUtil.i("Detected rapid put/remove of %s", key); } } } private static Map prefsCopy(Map src) { HashMap dst = new HashMap(src.size()); for (Map.Entry entry : src.entrySet()) { String key = entry.getKey(); Object value = entry.getValue(); if (value instanceof Set) { dst.put(key, shallowCopy((Set)value)); } else { dst.put(key, value); } } return dst; } private static Set shallowCopy(Set src) { HashSet dst = new HashSet(); for (T item : src) { dst.add(item); } return dst; } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/domstorage/SharedPreferencesHelper.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.domstorage; import android.content.Context; import android.content.SharedPreferences; import org.json.JSONArray; import org.json.JSONException; import javax.annotation.Nullable; import java.io.File; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Map.Entry; import java.util.Set; import java.util.TreeSet; public class SharedPreferencesHelper { private static final String PREFS_SUFFIX = ".xml"; private SharedPreferencesHelper() { } public static List getSharedPreferenceTags(Context context) { ArrayList tags = new ArrayList(); String rootPath = context.getApplicationInfo().dataDir + "/shared_prefs"; File root = new File(rootPath); if (root.exists()) { for (File file : root.listFiles()) { String fileName = file.getName(); if (fileName.endsWith(PREFS_SUFFIX)) { tags.add(fileName.substring(0, fileName.length() - PREFS_SUFFIX.length())); } } } Collections.sort(tags); return tags; } public static Set> getSharedPreferenceEntriesSorted(SharedPreferences preferences) { TreeSet> entries = new TreeSet<>(new Comparator>() { @Override public int compare(Entry lhs, Entry rhs) { return lhs.getKey().compareTo(rhs.getKey()); } }); entries.addAll(preferences.getAll().entrySet()); return entries; } public static String valueToString(Object value) { if (value != null) { if (value instanceof Set) { JSONArray array = new JSONArray(); for (String entry : (Set)value) { array.put(entry); } return array.toString(); } else { return value.toString(); } } else { return null; } } @Nullable public static Object valueFromString(String newValue, Object existingValue) throws IllegalArgumentException { if (existingValue instanceof Integer) { return Integer.parseInt(newValue); } else if (existingValue instanceof Long) { return Long.parseLong(newValue); } else if (existingValue instanceof Float) { return Float.parseFloat(newValue); } else if (existingValue instanceof Boolean) { return parseBoolean(newValue); } else if (existingValue instanceof String) { return newValue; } else if (existingValue instanceof Set) { try { JSONArray obj = new JSONArray(newValue); int objN = obj.length(); HashSet set = new HashSet(objN); for (int i = 0; i < objN; i++) { set.add(obj.getString(i)); } return set; } catch (JSONException e) { throw new IllegalArgumentException(e); } } else { throw new IllegalArgumentException( "Unsupported type: " + existingValue.getClass().getName()); } } private static Boolean parseBoolean(String s) throws IllegalArgumentException { if ("1".equals(s) || "true".equalsIgnoreCase(s)) { return Boolean.TRUE; } else if ("0".equals(s) || "false".equalsIgnoreCase(s)) { return Boolean.FALSE; } throw new IllegalArgumentException("Expected boolean, got " + s); } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/elements/AbstractChainedDescriptor.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.elements; import com.facebook.stetho.common.Accumulator; import com.facebook.stetho.common.ThreadBound; import com.facebook.stetho.common.Util; import com.facebook.stetho.inspector.protocol.module.DOM; import javax.annotation.Nullable; /** * This class derives from {@link Descriptor} and provides a canonical implementation of * {@link ChainedDescriptor}.

* * This class implements the thread safety enforcement policy prescribed by * {@link ThreadBound}. Namely, that {@link #verifyThreadAccess()}} needs to be called in the * prologue for every method. Your derived class SHOULD NOT call {@link #verifyThreadAccess()}} in * any of its on___() methods.

* * (NOTE: As an optimization, {@link #verifyThreadAccess()} is not actually called in the * prologue for every method. Instead, we rely on {@link DocumentProvider#getNodeDescriptor(Object)} * calling it in order to get most of our enforcement coverage. We still call * {@link #verifyThreadAccess()} in a few important methods such as {@link #hook(Object)} and * {@link #unhook(Object)} (anything that writes or is potentially really dangerous if misused).

* * @param the class that this descriptor will be describing for {@link DocumentProvider}, * {@link Document}, and ultimately {@link DOM}. */ public abstract class AbstractChainedDescriptor extends Descriptor implements ChainedDescriptor { private Descriptor mSuper; @Override public void setSuper(Descriptor superDescriptor) { Util.throwIfNull(superDescriptor); if (superDescriptor != mSuper) { if (mSuper != null) { throw new IllegalStateException(); } mSuper = superDescriptor; } } final Descriptor getSuper() { return mSuper; } @Override public final void hook(E element) { verifyThreadAccess(); mSuper.hook(element); onHook(element); } protected void onHook(E element) { } @Override public final void unhook(E element) { verifyThreadAccess(); onUnhook(element); mSuper.unhook(element); } protected void onUnhook(E element) { } @Override public final NodeType getNodeType(E element) { return onGetNodeType(element); } protected NodeType onGetNodeType(E element) { return mSuper.getNodeType(element); } @Override public final String getNodeName(E element) { return onGetNodeName(element); } protected String onGetNodeName(E element) { return mSuper.getNodeName(element); } @Override public final String getLocalName(E element) { return onGetLocalName(element); } protected String onGetLocalName(E element) { return mSuper.getLocalName(element); } @Override public final String getNodeValue(E element) { return onGetNodeValue(element); } @Nullable public String onGetNodeValue(E element) { return mSuper.getNodeValue(element); } @Override public final void getChildren(E element, Accumulator children) { mSuper.getChildren(element, children); onGetChildren(element, children); } protected void onGetChildren(E element, Accumulator children) { } @Override public final void getAttributes(E element, AttributeAccumulator attributes) { mSuper.getAttributes(element, attributes); onGetAttributes(element, attributes); } protected void onGetAttributes(E element, AttributeAccumulator attributes) { } @Override public final void setAttributesAsText(E element, String text) { onSetAttributesAsText(element, text); } protected void onSetAttributesAsText(E element, String text) { mSuper.setAttributesAsText(element, text); } @Override public final void getStyleRuleNames(E element, StyleRuleNameAccumulator accumulator) { mSuper.getStyleRuleNames(element, accumulator); onGetStyleRuleNames(element, accumulator); } protected void onGetStyleRuleNames(E element, StyleRuleNameAccumulator accumulator) { } @Override public final void getStyles(E element, String ruleName, StyleAccumulator accumulator) { mSuper.getStyles(element, ruleName, accumulator); onGetStyles(element, ruleName, accumulator); } protected void onGetStyles(E element, String ruleName, StyleAccumulator accumulator) { } @Override public final void setStyle(E element, String ruleName, String name, String value) { mSuper.setStyle(element, ruleName, name, value); onSetStyle(element, ruleName, name, value); } protected void onSetStyle(E element, String ruleName, String name, String value) { } @Override public void getComputedStyles(E element, ComputedStyleAccumulator accumulator) { mSuper.getComputedStyles(element, accumulator); onGetComputedStyles(element, accumulator); } protected void onGetComputedStyles(E element, ComputedStyleAccumulator accumulator) { } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/elements/AttributeAccumulator.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.elements; public interface AttributeAccumulator { void store(String name, String value); } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/elements/ChainedDescriptor.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.elements; import android.view.View; import android.view.ViewGroup; import android.widget.AbsListView; import android.widget.ListView; import com.facebook.stetho.common.Accumulator; /** * This interface marks a {@link Descriptor} in a way that is specially understood by * {@link DescriptorMap}. When registered for a particular class 'E', a {@link Descriptor} that * implements this interface will be chained (via {@link ChainedDescriptor#setSuper(Descriptor)}) to * the {@link Descriptor} that is registered for the super class of E. If the super class of E * doesn't have a registration, then the super-super class will be used (and so on). This allows you * to implement {@link Descriptor} for any class in an inheritance hierarchy without having to * couple it (via direct inheritance) to the super-class' {@link Descriptor}.

* * To understand why this is useful, let's say you wanted to write a {@link Descriptor} for * {@link ListView}. You have three options:

* * The first option is to derive directly from {@link Descriptor} and write code to describe * everything about instances of {@link ListView}, including details that are exposed by super * classes such as {@link ViewGroup}, {@link View}, and even {@link Object}. This isn't generally * a very good choice because it would require a lot of duplicated code amongst many descriptor * implementations.

* * The second option is to derive your 'ListViewDescriptor' from * {@link com.facebook.stetho.inspector.elements.android.ViewGroupDescriptor} and only implement * code to describe how {@link ListView} differs from {@link ViewGroup}. This will result in a class * hierarchy that is parallel to the one that you are describing, but is also not a good choice for * two reasons (let's assume for the moment that * {@link com.facebook.stetho.inspector.elements.android.ViewGroupDescriptor} is deriving from * {@link com.facebook.stetho.inspector.elements.android.ViewDescriptor}). The first problem is that * you will need to write code for aggregating results from the super-class in methods such as * {@link Descriptor#getChildren(Object, Accumulator)} and * {@link Descriptor#getAttributes(Object, AttributeAccumulator)}. The second problem is that you'd * end up with a log of fragility if you ever want to implement a descriptor for classes that are * in-between {@link ViewGroup} and {@link ListView}, e.g. {@link AbsListView}. Any descriptor that * derived from {@link com.facebook.stetho.inspector.elements.android.ViewGroupDescriptor} and * described a class deriving from {@link AbsListView} would have to be modified to now derive from * 'AbsListViewDescriptor'.

* * The third option is to implement {@link ChainedDescriptor} (e.g. by deriving from * {@link AbstractChainedDescriptor}) which solves all of these issues for you.

*/ public interface ChainedDescriptor { void setSuper(Descriptor superDescriptor); } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/elements/ComputedStyleAccumulator.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.elements; public interface ComputedStyleAccumulator { void store(String name, String value); } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/elements/Descriptor.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.elements; import com.facebook.stetho.common.ThreadBound; import com.facebook.stetho.common.UncheckedCallable; import com.facebook.stetho.common.Util; import javax.annotation.Nullable; import java.util.HashMap; import java.util.Map; public abstract class Descriptor implements NodeDescriptor { private Host mHost; protected Descriptor() { } final void initialize(Host host) { Util.throwIfNull(host); Util.throwIfNotNull(mHost); mHost = host; } final boolean isInitialized() { return mHost != null; } protected final Host getHost() { return mHost; } @Override public final boolean checkThreadAccess() { return getHost().checkThreadAccess(); } @Override public final void verifyThreadAccess() { getHost().verifyThreadAccess(); } @Override public final V postAndWait(UncheckedCallable c) { return getHost().postAndWait(c); } @Override public final void postAndWait(Runnable r) { getHost().postAndWait(r); } @Override public final void postDelayed(Runnable r, long delayMillis) { getHost().postDelayed(r, delayMillis); } @Override public final void removeCallbacks(Runnable r) { getHost().removeCallbacks(r); } /** * Parses the text argument text from DOM.setAttributeAsText() * Text will be in the format "attribute1=\"Value 1\" attribute2=\"Value2\"" * @param text the text argument to be parsed * @return a map of attributes to their respective values to be set. */ protected static Map parseSetAttributesAsTextArg(String text) { String value = ""; String key = ""; StringBuilder buffer = new StringBuilder(); Map keyValuePairs = new HashMap<>(); boolean isInsideQuotes = false; for (int i = 0, N = text.length(); i < N; ++i) { final char c = text.charAt(i); if (c == '=') { key = buffer.toString(); buffer.setLength(0); } else if (c == '\"') { if (isInsideQuotes) { value = buffer.toString(); buffer.setLength(0); } isInsideQuotes = !isInsideQuotes; } else if (c == ' ' && !isInsideQuotes) { keyValuePairs.put(key, value); } else { buffer.append(c); } } if (!key.isEmpty() && !value.isEmpty()) { keyValuePairs.put(key, value); } return keyValuePairs; } public interface Host extends ThreadBound { @Nullable public Descriptor getDescriptor(@Nullable Object element); public void onAttributeModified( Object element, String name, String value); public void onAttributeRemoved( Object element, String name); } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/elements/DescriptorMap.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.elements; import com.facebook.stetho.common.Util; import java.util.IdentityHashMap; import java.util.Map; import javax.annotation.Nullable; public final class DescriptorMap implements DescriptorRegistrar { private final Map, Descriptor> mMap = new IdentityHashMap<>(); private boolean mIsInitializing; private Descriptor.Host mHost; public DescriptorMap beginInit() { Util.throwIf(mIsInitializing); mIsInitializing = true; return this; } @Override public DescriptorMap registerDescriptor(Class elementClass, Descriptor descriptor) { Util.throwIfNull(elementClass); Util.throwIfNull(descriptor); Util.throwIf(descriptor.isInitialized()); Util.throwIfNot(mIsInitializing); // Cannot register two descriptors for one class if (mMap.containsKey(elementClass)) { throw new UnsupportedOperationException(); } // Cannot reuse one descriptor for two classes if (mMap.containsValue(descriptor)) { throw new UnsupportedOperationException(); } mMap.put(elementClass, descriptor); return this; } public DescriptorMap setHost(Descriptor.Host host) { Util.throwIfNull(host); Util.throwIfNot(mIsInitializing); Util.throwIfNotNull(mHost); mHost = host; return this; } public DescriptorMap endInit() { Util.throwIfNot(mIsInitializing); Util.throwIfNull(mHost); mIsInitializing = false; for (final Class elementClass : mMap.keySet()) { final Descriptor descriptor = mMap.get(elementClass); if (descriptor instanceof ChainedDescriptor) { final ChainedDescriptor chainedDescriptor = (ChainedDescriptor) descriptor; Class superClass = elementClass.getSuperclass(); Descriptor superDescriptor = getImpl(superClass); chainedDescriptor.setSuper(superDescriptor); } descriptor.initialize(mHost); } return this; } @Nullable public Descriptor get(Class elementClass) { Util.throwIfNull(elementClass); Util.throwIf(mIsInitializing); return getImpl(elementClass); } @Nullable private Descriptor getImpl(final Class elementClass) { Class theClass = elementClass; while (theClass != null) { Descriptor descriptor = mMap.get(theClass); if (descriptor != null) { return descriptor; } theClass = theClass.getSuperclass(); } return null; } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/elements/DescriptorProvider.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.elements; public interface DescriptorProvider { void registerDescriptor(DescriptorRegistrar registrar); } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/elements/DescriptorRegistrar.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.elements; public interface DescriptorRegistrar { DescriptorRegistrar registerDescriptor(Class elementClass, Descriptor descriptor); } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/elements/Document.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.elements; import android.os.SystemClock; import com.facebook.stetho.common.Accumulator; import com.facebook.stetho.common.ArrayListAccumulator; import com.facebook.stetho.common.LogUtil; import com.facebook.stetho.common.Util; import com.facebook.stetho.inspector.helper.ObjectIdMapper; import com.facebook.stetho.inspector.helper.ThreadBoundProxy; import javax.annotation.Nullable; import javax.annotation.concurrent.GuardedBy; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Queue; import java.util.regex.Pattern; public final class Document extends ThreadBoundProxy { private final DocumentProviderFactory mFactory; private final ObjectIdMapper mObjectIdMapper; private final Queue mCachedUpdateQueue; private DocumentProvider mDocumentProvider; private ShadowDocument mShadowDocument; private UpdateListenerCollection mUpdateListeners; private ChildEventingList mCachedChildEventingList; private ArrayListAccumulator mCachedChildrenAccumulator; private AttributeListAccumulator mCachedAttributeAccumulator; @GuardedBy("this") private int mReferenceCounter; public Document(DocumentProviderFactory factory) { super(factory); mFactory = factory; mObjectIdMapper = new DocumentObjectIdMapper(); mReferenceCounter = 0; mUpdateListeners = new UpdateListenerCollection(); mCachedUpdateQueue = new ArrayDeque<>(); } public synchronized void addRef() { if (mReferenceCounter++ == 0) { init(); } } public synchronized void release() { if (mReferenceCounter > 0) { if (--mReferenceCounter == 0) { cleanUp(); } } } private void init() { mDocumentProvider = mFactory.create(); mDocumentProvider.postAndWait(new Runnable() { @Override public void run() { mShadowDocument = new ShadowDocument(mDocumentProvider.getRootElement()); createShadowDocumentUpdate().commit(); mDocumentProvider.setListener(new ProviderListener()); } }); } private void cleanUp() { mDocumentProvider.postAndWait(new Runnable() { @Override public void run() { mDocumentProvider.setListener(null); mShadowDocument = null; mObjectIdMapper.clear(); mDocumentProvider.dispose(); mDocumentProvider = null; } }); mUpdateListeners.clear(); } public void addUpdateListener(UpdateListener updateListener) { mUpdateListeners.add(updateListener); } public void removeUpdateListener(UpdateListener updateListener) { mUpdateListeners.remove(updateListener); } public @Nullable NodeDescriptor getNodeDescriptor(Object element) { verifyThreadAccess(); return mDocumentProvider.getNodeDescriptor(element); } public void highlightElement(Object element, int color) { verifyThreadAccess(); mDocumentProvider.highlightElement(element, color); } public void hideHighlight() { verifyThreadAccess(); mDocumentProvider.hideHighlight(); } public void setInspectModeEnabled(boolean enabled) { verifyThreadAccess(); mDocumentProvider.setInspectModeEnabled(enabled); } public @Nullable Integer getNodeIdForElement(Object element) { // We don't actually call verifyThreadAccess() for performance. //verifyThreadAccess(); return mObjectIdMapper.getIdForObject(element); } public @Nullable Object getElementForNodeId(int id) { // We don't actually call verifyThreadAccess() for performance. //verifyThreadAccess(); return mObjectIdMapper.getObjectForId(id); } public void setAttributesAsText(Object element, String text) { verifyThreadAccess(); mDocumentProvider.setAttributesAsText(element, text); } public void getElementStyleRuleNames(Object element, StyleRuleNameAccumulator accumulator) { NodeDescriptor nodeDescriptor = getNodeDescriptor(element); nodeDescriptor.getStyleRuleNames(element, accumulator); } public void getElementStyles(Object element, String ruleName, StyleAccumulator accumulator) { NodeDescriptor nodeDescriptor = getNodeDescriptor(element); nodeDescriptor.getStyles(element, ruleName, accumulator); } public void setElementStyle(Object element, String ruleName, String name, String value) { NodeDescriptor nodeDescriptor = getNodeDescriptor(element); nodeDescriptor.setStyle(element, ruleName, name, value); } public void getElementComputedStyles(Object element, ComputedStyleAccumulator styleAccumulator) { NodeDescriptor nodeDescriptor = getNodeDescriptor(element); nodeDescriptor.getComputedStyles(element, styleAccumulator); } public DocumentView getDocumentView() { verifyThreadAccess(); return mShadowDocument; } public Object getRootElement() { verifyThreadAccess(); Object rootElement = mDocumentProvider.getRootElement(); if (rootElement == null) { // null for rootElement is not allowed. We could support it, but our current // implementation won't ever run into this, so let's punt on it for now. throw new IllegalStateException(); } if (rootElement != mShadowDocument.getRootElement()) { // We don't support changing the root element. This is handled differently by the // protocol than updates to an existing DOM, and we don't have any case in our // current implementation that causes this to happen, so let's punt on it for now. throw new IllegalStateException(); } return rootElement; } public void findMatchingElements(String query, Accumulator matchedIds) { verifyThreadAccess(); final Pattern queryPattern = Pattern.compile(Pattern.quote(query), Pattern.CASE_INSENSITIVE); final Object rootElement = mDocumentProvider.getRootElement(); findMatches(rootElement, queryPattern, matchedIds); } private void findMatches(Object element, Pattern queryPattern, Accumulator matchedIds) { final ElementInfo info = mShadowDocument.getElementInfo(element); for (int i = 0, size = info.children.size(); i < size; i++) { final Object childElement = info.children.get(i); if (doesElementMatch(childElement, queryPattern)) { matchedIds.store(mObjectIdMapper.getIdForObject(childElement)); } findMatches(childElement, queryPattern, matchedIds); } } private boolean doesElementMatch(Object element, Pattern queryPattern) { AttributeListAccumulator accumulator = acquireCachedAttributeAccumulator(); NodeDescriptor descriptor = mDocumentProvider.getNodeDescriptor(element); descriptor.getAttributes(element, accumulator); for (int i = 0, N = accumulator.size(); i < N; i++) { if (queryPattern.matcher(accumulator.get(i)).find()) { releaseCachedAttributeAccumulator(accumulator); return true; } } releaseCachedAttributeAccumulator(accumulator); return queryPattern.matcher(descriptor.getNodeName(element)).find(); } private ChildEventingList acquireChildEventingList( Object parentElement, DocumentView documentView) { ChildEventingList childEventingList = mCachedChildEventingList; if (childEventingList == null) { childEventingList = new ChildEventingList(); } mCachedChildEventingList = null; childEventingList.acquire(parentElement, documentView); return childEventingList; } private void releaseChildEventingList(ChildEventingList childEventingList) { childEventingList.release(); if (mCachedChildEventingList == null) { mCachedChildEventingList = childEventingList; } } private AttributeListAccumulator acquireCachedAttributeAccumulator() { AttributeListAccumulator accumulator = mCachedAttributeAccumulator; if (accumulator == null) { accumulator = new AttributeListAccumulator(); } mCachedChildrenAccumulator = null; return accumulator; } private void releaseCachedAttributeAccumulator(AttributeListAccumulator accumulator) { accumulator.clear(); if (mCachedAttributeAccumulator == null) { mCachedAttributeAccumulator = accumulator; } } private ArrayListAccumulator acquireChildrenAccumulator() { ArrayListAccumulator accumulator = mCachedChildrenAccumulator; if (accumulator == null) { accumulator = new ArrayListAccumulator<>(); } mCachedChildrenAccumulator = null; return accumulator; } private void releaseChildrenAccumulator(ArrayListAccumulator accumulator) { accumulator.clear(); if (mCachedChildrenAccumulator == null) { mCachedChildrenAccumulator = accumulator; } } private ShadowDocument.Update createShadowDocumentUpdate() { verifyThreadAccess(); if (mDocumentProvider.getRootElement() != mShadowDocument.getRootElement()) { throw new IllegalStateException(); } ArrayListAccumulator childrenAccumulator = acquireChildrenAccumulator(); ShadowDocument.UpdateBuilder updateBuilder = mShadowDocument.beginUpdate(); mCachedUpdateQueue.add(mDocumentProvider.getRootElement()); while (!mCachedUpdateQueue.isEmpty()) { final Object element = mCachedUpdateQueue.remove(); NodeDescriptor descriptor = mDocumentProvider.getNodeDescriptor(element); mObjectIdMapper.putObject(element); descriptor.getChildren(element, childrenAccumulator); for (int i = 0, size = childrenAccumulator.size(); i < size; ++i) { Object child = childrenAccumulator.get(i); if (child != null) { mCachedUpdateQueue.add(child); } else { // This could be indicative of a bug in Stetho code, but could also be caused by a // custom element of some kind, e.g. ViewGroup. Let's not allow it to kill the hosting // app. LogUtil.e( "%s.getChildren() emitted a null child at position %s for element %s", descriptor.getClass().getName(), Integer.toString(i), element); childrenAccumulator.remove(i); --i; --size; } } updateBuilder.setElementChildren(element, childrenAccumulator); childrenAccumulator.clear(); } releaseChildrenAccumulator(childrenAccumulator); return updateBuilder.build(); } private void updateTree() { long startTimeMs = SystemClock.elapsedRealtime(); ShadowDocument.Update docUpdate = createShadowDocumentUpdate(); boolean isEmpty = docUpdate.isEmpty(); if (isEmpty) { docUpdate.abandon(); } else { applyDocumentUpdate(docUpdate); } long deltaMs = SystemClock.elapsedRealtime() - startTimeMs; LogUtil.d( "Document.updateTree() completed in %s ms%s", Long.toString(deltaMs), isEmpty ? " (no changes)" : ""); } private void applyDocumentUpdate(final ShadowDocument.Update docUpdate) { // TODO: it'd be nice if we could delegate our calls into mPeerManager.sendNotificationToPeers() // to a background thread so as to offload the UI from JSON serialization stuff // Applying the ShadowDocument.Update is done in five stages: // Stage 1: any elements that have been disconnected from the tree, and any elements in those // sub-trees which have not been reconnected to the tree, should be garbage collected. For now // we gather a list of garbage element IDs which we use in stages 2 to test a changed element // to see if it's also garbage. Then during stage 3 we use this list to unhook all of the // garbage elements. // This is used to collect the garbage element IDs in stage 1. It is sorted before stage 2 so // that it can use a binary search as a quick "contains()" method. // Note that this could be accomplished in a simpler way by employing a HashSet and // storing the element Objects. However, HashSet wraps HashMap and we would have a lot more // allocations (Map.Entry, iterator during stage 3) and thus GC pressure. // Using SparseArray wouldn't be good because it ensures sorted ordering as you go, but we don't // need that during stage 1. Using ArrayList with int boxing is fine because the Integers are // already boxed inside of mObjectIdMapper and we make sure to reuse that allocation. final ArrayList garbageElementIds = new ArrayList<>(); docUpdate.getGarbageElements(new Accumulator() { @Override public void store(Object element) { Integer nodeId = Util.throwIfNull(mObjectIdMapper.getIdForObject(element)); ElementInfo newElementInfo = docUpdate.getElementInfo(element); // Only raise onChildNodeRemoved for the root of a disconnected tree. The remainder of the // sub-tree is included automatically, so we don't need to send events for those. if (newElementInfo.parentElement == null) { ElementInfo oldElementInfo = mShadowDocument.getElementInfo(element); int parentNodeId = mObjectIdMapper.getIdForObject(oldElementInfo.parentElement); mUpdateListeners.onChildNodeRemoved(parentNodeId, nodeId); } garbageElementIds.add(nodeId); } }); Collections.sort(garbageElementIds); // Stage 2: remove all elements that have been reparented. Otherwise we get into trouble if we // transmit an event to insert under the new parent before we've transmitted an event to remove // it from the old parent. The removal event is ignored because the parent doesn't match the // listener's expectations, so we get ghost elements that are stuck and can't be exorcised. docUpdate.getChangedElements(new Accumulator() { @Override public void store(Object element) { Integer nodeId = Util.throwIfNull(mObjectIdMapper.getIdForObject(element)); // Skip garbage elements if (Collections.binarySearch(garbageElementIds, nodeId) >= 0) { return; } // Skip new elements final ElementInfo oldElementInfo = mShadowDocument.getElementInfo(element); if (oldElementInfo == null) { return; } final ElementInfo newElementInfo = docUpdate.getElementInfo(element); if (newElementInfo.parentElement != oldElementInfo.parentElement) { int parentNodeId = mObjectIdMapper.getIdForObject(oldElementInfo.parentElement); mUpdateListeners.onChildNodeRemoved(parentNodeId, nodeId); } } }); // Stage 3: unhook garbage elements for (int i = 0, N = garbageElementIds.size(); i < N; ++i) { mObjectIdMapper.removeObjectById(garbageElementIds.get(i)); } // Stage 4: transmit all other changes to our listener. This includes inserting reparented // elements that we removed in the 2nd stage. docUpdate.getChangedElements(new Accumulator() { private final HashSet listenerInsertedElements = new HashSet<>(); private Accumulator insertedElements = new Accumulator() { @Override public void store(Object element) { if (docUpdate.isElementChanged(element)) { // We only need to track changed elements because unchanged elements will never be // encountered by the code below, in store(), which uses this Set to skip elements that // don't need to be processed. listenerInsertedElements.add(element); } } }; @Override public void store(Object element) { if (!mObjectIdMapper.containsObject(element)) { // The element was garbage and has already been removed. At this stage that's okay and we // just skip it and continue forward with the algorithm. return; } if (listenerInsertedElements.contains(element)) { // This element was already transmitted in its entirety by an onChildNodeInserted event. // Trying to send any further updates about it is both unnecessary and incorrect (we'd // end up with duplicated elements and really bad performance). return; } final ElementInfo oldElementInfo = mShadowDocument.getElementInfo(element); final ElementInfo newElementInfo = docUpdate.getElementInfo(element); final List oldChildren = (oldElementInfo != null) ? oldElementInfo.children : Collections.emptyList(); final List newChildren = newElementInfo.children; // This list is representative of our listener's view of the Document (ultimately, this // means Chrome DevTools). We need to sync it up with newChildren. ChildEventingList listenerChildren = acquireChildEventingList(element, docUpdate); for (int i = 0, N = oldChildren.size(); i < N; ++i) { final Object childElement = oldChildren.get(i); if (mObjectIdMapper.containsObject(childElement)) { final ElementInfo newChildElementInfo = docUpdate.getElementInfo(childElement); if (newChildElementInfo != null && newChildElementInfo.parentElement != element) { // This element was reparented, so we already told our listener to remove it. } else { listenerChildren.add(childElement); } } } updateListenerChildren(listenerChildren, newChildren, insertedElements); releaseChildEventingList(listenerChildren); } }); // Stage 5: Finally, commit the update to the ShadowDocument. docUpdate.commit(); } private static void updateListenerChildren( ChildEventingList listenerChildren, List newChildren, Accumulator insertedElements) { int index = 0; while (index <= listenerChildren.size()) { // Insert new items that were added to the end of the list if (index == listenerChildren.size()) { if (index == newChildren.size()) { break; } final Object newElement = newChildren.get(index); listenerChildren.addWithEvent(index, newElement, insertedElements); ++index; continue; } // Remove old items that were removed from the end of the list if (index == newChildren.size()) { listenerChildren.removeWithEvent(index); continue; } final Object listenerElement = listenerChildren.get(index); final Object newElement = newChildren.get(index); // This slot has exactly what we need to have here. if (listenerElement == newElement) { ++index; continue; } int newElementListenerIndex = listenerChildren.indexOf(newElement); if (newElementListenerIndex == -1) { listenerChildren.addWithEvent(index, newElement, insertedElements); ++index; continue; } // TODO: use longest common substring to decide whether to // 1) remove(newElementListenerIndex)-then-add(index), or // 2) remove(index) and let a subsequent loop iteration do add() (that is, when index // catches up the current value of newElementListenerIndex) // Neither one of these is the best strategy -- it depends on context. listenerChildren.removeWithEvent(newElementListenerIndex); listenerChildren.addWithEvent(index, newElement, insertedElements); ++index; } } /** * A private implementation of {@link List} that transmits our changes to our listener (and, * ultimately, to the DevTools client). */ private final class ChildEventingList extends ArrayList { private Object mParentElement = null; private int mParentNodeId = -1; private DocumentView mDocumentView; public void acquire(Object parentElement, DocumentView documentView) { mParentElement = parentElement; mParentNodeId = (mParentElement == null) ? -1 : mObjectIdMapper.getIdForObject(mParentElement); mDocumentView = documentView; } public void release() { clear(); mParentElement = null; mParentNodeId = -1; mDocumentView = null; } public void addWithEvent(int index, Object element, Accumulator insertedElements) { Object previousElement = (index == 0) ? null : get(index - 1); int previousNodeId = (previousElement == null) ? -1 : mObjectIdMapper.getIdForObject(previousElement); add(index, element); mUpdateListeners.onChildNodeInserted( mDocumentView, element, mParentNodeId, previousNodeId, insertedElements); } public void removeWithEvent(int index) { Object element = remove(index); int nodeId = mObjectIdMapper.getIdForObject(element); mUpdateListeners.onChildNodeRemoved(mParentNodeId, nodeId); } } private class UpdateListenerCollection implements UpdateListener { private final List mListeners; private volatile UpdateListener[] mListenersSnapshot; public UpdateListenerCollection() { mListeners = new ArrayList<>(); } public synchronized void add(UpdateListener listener) { mListeners.add(listener); mListenersSnapshot = null; } public synchronized void remove(UpdateListener listener) { mListeners.remove(listener); mListenersSnapshot = null; } public synchronized void clear() { mListeners.clear(); mListenersSnapshot = null; } private UpdateListener[] getListenersSnapshot() { while (true) { final UpdateListener[] listenersSnapshot = mListenersSnapshot; if (listenersSnapshot != null) { return listenersSnapshot; } synchronized (this) { if (mListenersSnapshot == null) { mListenersSnapshot = mListeners.toArray(new UpdateListener[mListeners.size()]); return mListenersSnapshot; } } } } @Override public void onAttributeModified(Object element, String name, String value) { for (UpdateListener listener : getListenersSnapshot()) { listener.onAttributeModified(element, name, value); } } @Override public void onAttributeRemoved(Object element, String name) { for (UpdateListener listener : getListenersSnapshot()) { listener.onAttributeRemoved(element, name); } } @Override public void onInspectRequested(Object element) { for (UpdateListener listener : getListenersSnapshot()) { listener.onInspectRequested(element); } } @Override public void onChildNodeRemoved(int parentNodeId, int nodeId) { for (UpdateListener listener : getListenersSnapshot()) { listener.onChildNodeRemoved(parentNodeId, nodeId); } } @Override public void onChildNodeInserted( DocumentView view, Object element, int parentNodeId, int previousNodeId, Accumulator insertedItems) { for (UpdateListener listener : getListenersSnapshot()) { listener.onChildNodeInserted(view, element, parentNodeId, previousNodeId, insertedItems); } } } public interface UpdateListener { void onAttributeModified(Object element, String name, String value); void onAttributeRemoved(Object element, String name); void onInspectRequested(Object element); void onChildNodeRemoved( int parentNodeId, int nodeId); void onChildNodeInserted( DocumentView view, Object element, int parentNodeId, int previousNodeId, Accumulator insertedItems); } private final class DocumentObjectIdMapper extends ObjectIdMapper { @Override protected void onMapped(Object object, int id) { verifyThreadAccess(); NodeDescriptor descriptor = mDocumentProvider.getNodeDescriptor(object); descriptor.hook(object); } @Override protected void onUnmapped(Object object, int id) { verifyThreadAccess(); NodeDescriptor descriptor = mDocumentProvider.getNodeDescriptor(object); descriptor.unhook(object); } } private final class ProviderListener implements DocumentProviderListener { @Override public void onPossiblyChanged() { updateTree(); } @Override public void onAttributeModified(Object element, String name, String value) { verifyThreadAccess(); mUpdateListeners.onAttributeModified(element, name, value); } @Override public void onAttributeRemoved(Object element, String name) { verifyThreadAccess(); mUpdateListeners.onAttributeRemoved(element, name); } @Override public void onInspectRequested(Object element) { verifyThreadAccess(); mUpdateListeners.onInspectRequested(element); } } public static final class AttributeListAccumulator extends ArrayList implements AttributeAccumulator { @Override public void store(String name, String value) { add(name); add(value); } } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/elements/DocumentProvider.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.elements; import com.facebook.stetho.common.ThreadBound; import javax.annotation.Nullable; /** * Provides a document that can be rendered in Chrome's Elements tab (conforming loosely to the * W3C DOM to the degree specified in this API). * * @see DocumentProviderFactory */ public interface DocumentProvider extends ThreadBound { void setListener(DocumentProviderListener listener); void dispose(); @Nullable Object getRootElement(); @Nullable NodeDescriptor getNodeDescriptor(@Nullable Object element); void highlightElement(Object element, int color); void hideHighlight(); void setInspectModeEnabled(boolean enabled); void setAttributesAsText(Object element, String text); } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/elements/DocumentProviderFactory.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.elements; import com.facebook.stetho.common.ThreadBound; /** * Factory mechanism to dynamically construct the document provider. This allows for lazy * initialization and memory cleanup when DevTools instances disconnect. */ public interface DocumentProviderFactory extends ThreadBound { DocumentProvider create(); } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/elements/DocumentProviderListener.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.elements; public interface DocumentProviderListener { void onPossiblyChanged(); void onAttributeModified( Object element, String name, String value); void onAttributeRemoved( Object element, String name); void onInspectRequested( Object element); } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/elements/DocumentView.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.elements; public interface DocumentView { Object getRootElement(); ElementInfo getElementInfo(Object element); } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/elements/ElementInfo.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.elements; import com.facebook.stetho.common.ListUtil; import com.facebook.stetho.common.Util; import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; import java.util.List; @Immutable public final class ElementInfo { public final Object element; public @Nullable final Object parentElement; public final List children; ElementInfo( Object element, @Nullable Object parentElement, List children) { this.element = Util.throwIfNull(element); this.parentElement = parentElement; this.children = ListUtil.copyToImmutableList(children); } @Override public boolean equals(Object o) { if (o == this) { return true; } if (o instanceof ElementInfo) { ElementInfo other = (ElementInfo) o; return this.element == other.element && this.parentElement == other.parentElement && ListUtil.identityEquals(this.children, other.children); } return false; } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/elements/NodeDescriptor.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.elements; import com.facebook.stetho.common.Accumulator; import com.facebook.stetho.common.ThreadBound; import javax.annotation.Nullable; public interface NodeDescriptor extends ThreadBound { void hook(E element); void unhook(E element); NodeType getNodeType(E element); String getNodeName(E element); String getLocalName(E element); @Nullable String getNodeValue(E element); void getChildren(E element, Accumulator children); void getAttributes(E element, AttributeAccumulator attributes); void setAttributesAsText(E element, String text); void getStyleRuleNames(E element, StyleRuleNameAccumulator accumulator); void getStyles(E element, String ruleName, StyleAccumulator accumulator); void setStyle(E element, String ruleName, String name, String value); void getComputedStyles(E element, ComputedStyleAccumulator accumulator); } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/elements/NodeType.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.elements; import com.facebook.stetho.json.annotation.JsonValue; public enum NodeType { ELEMENT_NODE(1), TEXT_NODE(3), PROCESSING_INSTRUCTION_NODE(7), COMMENT_NODE(8), DOCUMENT_NODE(9), DOCUMENT_TYPE_NODE(10), DOCUMENT_FRAGMENT_NODE(11); private final int mValue; private NodeType(int value) { mValue = value; } @JsonValue public int getProtocolValue() { return mValue; } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/elements/ObjectDescriptor.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.elements; import com.facebook.stetho.common.Accumulator; public final class ObjectDescriptor extends Descriptor { @Override public void hook(Object element) { } @Override public void unhook(Object element) { } @Override public NodeType getNodeType(Object element) { return NodeType.ELEMENT_NODE; } @Override public String getNodeName(Object element) { return element.getClass().getName(); } @Override public String getLocalName(Object element) { return getNodeName(element); } @Override public String getNodeValue(Object element) { return null; } @Override public void getChildren(Object element, Accumulator children) { } @Override public void getAttributes(Object element, AttributeAccumulator attributes) { } @Override public void setAttributesAsText(Object element, String text) { } @Override public void getStyleRuleNames(Object element, StyleRuleNameAccumulator accumulator) { } @Override public void getStyles(Object element, String ruleName, StyleAccumulator accumulator) { } @Override public void setStyle(Object element, String ruleName, String name, String value) { } @Override public void getComputedStyles(Object element, ComputedStyleAccumulator accumulator) { } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/elements/Origin.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.elements; import com.facebook.stetho.json.annotation.JsonValue; public enum Origin { INJECTED("injected"), USER_AGENT("user-agent"), INSPECTOR("inspector"), REGULAR("regular"); private final String mValue; Origin(String value) { mValue = value; } @JsonValue public String getProtocolValue() { return mValue; } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/elements/ShadowDocument.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.elements; import android.app.Activity; import com.facebook.stetho.common.Accumulator; import com.facebook.stetho.common.ListUtil; import com.facebook.stetho.common.Util; import java.util.ArrayDeque; import java.util.Collections; import java.util.HashSet; import java.util.IdentityHashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Queue; import java.util.Set; public final class ShadowDocument implements DocumentView { private final Object mRootElement; private final IdentityHashMap mElementToInfoMap = new IdentityHashMap<>(); private boolean mIsUpdating; public ShadowDocument(Object rootElement) { mRootElement = Util.throwIfNull(rootElement); } @Override public Object getRootElement() { return mRootElement; } @Override public ElementInfo getElementInfo(Object element) { return mElementToInfoMap.get(element); } public UpdateBuilder beginUpdate() { if (mIsUpdating) { throw new IllegalStateException(); } mIsUpdating = true; return new UpdateBuilder(); } public final class UpdateBuilder { /** * We use a {@link LinkedHashMap} to preserve ordering between * {@link UpdateBuilder#setElementChildren(Object, List)} and * {@link Update#getChangedElements(Accumulator)}. This isn't needed for correctness but it * significantly improves performance.

* * Transmitting DOM updates to Chrome works best if we can do it in top-down order because it * allows us to skip processing (and, more importantly, transmission) of an element that was * already transmitted in a previous DOM.childNodeInserted event (i.o.w. we can skip * transmission of E2 if it was already bundled up in E1's event, where E2 is any element in * E1's sub-tree). DOM.childNodeInserted transmits the node being inserted by-value, so it takes * time and space proportional to the size of that node's sub-tree. This means the difference * between O(n^2) and O(n) time for transmitting updates to Chrome.

* * We currently only have one implementation of {@link DocumentProvider}, * {@link com.facebook.stetho.inspector.elements.android.AndroidDocumentProvider}, and it * already supplies element changes in top-down order. Because of this, we can just use * {@link LinkedHashMap} instead of adding some kind of post-process sorting of the elements to * put them in that order. If we reach a point where we can't or shouldn't rely on elements * being forwarded to us in top-down order, then we should change this field to an * {@link IdentityHashMap} and sort them before relaying them via * {@link Update#getChangedElements(Accumulator)}.

* * When a large sub-tree is added (e.g. starting a new {@link Activity}), the use of * {@link LinkedHashMap} instead of {@link IdentityHashMap} can mean the difference between an * update taking 500ms versus taking more than 30 seconds.

* * Technically we actually want something like a LinkedIdentityHashMap because we do want * to key off of object identity instead of allowing for the possibility of value identity. * Given the difference in performance, however, the risk of potential protocol abuse seems * reasonable.

*/ private final Map mElementToInfoChangesMap = new LinkedHashMap<>(); /** * This contains every element in {@link #mElementToInfoChangesMap} whose * {@link ElementInfo#parentElement} is null. {@link ShadowDocument} provides access to a tree, which * means it has a single root (only one element with a null parent). During an update, however, * the DOM can be conceptually thought of as being a forest. The true root is identified by * {@link #mRootElement}, and all other roots identify disconnected trees full of elements that * must be garbage collected. */ private final HashSet mRootElementChanges = new HashSet<>(); /** * This is used during {@link #setElementChildren}. We allocate 1 on-demand and reuse it. */ private HashSet mCachedNotNewChildrenSet; public void setElementChildren(Object element, List children) { // If we receive redundant information, then nothing needs to be done. ElementInfo changesElementInfo = mElementToInfoChangesMap.get(element); if (changesElementInfo != null && ListUtil.identityEquals(children, changesElementInfo.children)) { return; } ElementInfo oldElementInfo = mElementToInfoMap.get(element); if (changesElementInfo == null && oldElementInfo != null && ListUtil.identityEquals(children, oldElementInfo.children)) { return; } ElementInfo newElementInfo; if (changesElementInfo != null && oldElementInfo != null && oldElementInfo.parentElement == changesElementInfo.parentElement && ListUtil.identityEquals(children, oldElementInfo.children)) { // setElementChildren() was already called for element with changes during this // transaction, but now we're being told that the children should match the old view. // So we should actually remove the change entry. newElementInfo = mElementToInfoMap.get(element); mElementToInfoChangesMap.remove(element); } else { Object parentElement = (changesElementInfo != null) ? changesElementInfo.parentElement : (oldElementInfo != null) ? oldElementInfo.parentElement : null; newElementInfo = new ElementInfo(element, parentElement, children); mElementToInfoChangesMap.put(element, newElementInfo); } // At this point, newElementInfo is either equal to oldElementInfo because we've reverted // back to the same data that's in the old view of the tree, or it's a brand new object with // brand new changes (it's different than both of oldElementInfo and changesElementInfo). // Next, set the parentElement to null for child elements that have been removed from // element's children. We must be careful not to set a parentElement to null if that child has // already been moved to be the child of a different element. e.g., // setElementChildren(E, { A, B, C}) // ... // setElementChildren(F, { A }) // setElementChildren(E, { B, C }) (don't mark A's parent as null in this case) // notNewChildrenSet = (oldChildren + changesChildren) - newChildren HashSet notNewChildrenSet = acquireNotNewChildrenHashSet(); if (oldElementInfo != null && oldElementInfo.children != newElementInfo.children) { for (int i = 0, N = oldElementInfo.children.size(); i < N; ++i) { final Object childElement = oldElementInfo.children.get(i); notNewChildrenSet.add(childElement); } } if (changesElementInfo != null && changesElementInfo.children != newElementInfo.children) { for (int i = 0, N = changesElementInfo.children.size(); i < N; ++i) { final Object childElement = changesElementInfo.children.get(i); notNewChildrenSet.add(childElement); } } for (int i = 0, N = newElementInfo.children.size(); i < N; ++i) { final Object childElement = newElementInfo.children.get(i); setElementParent(childElement, element); notNewChildrenSet.remove(childElement); } for (Object childElement : notNewChildrenSet) { final ElementInfo childChangesElementInfo = mElementToInfoChangesMap.get(childElement); if (childChangesElementInfo != null && childChangesElementInfo.parentElement != element) { // do nothing. this childElement was moved to be the child of another element. continue; } final ElementInfo oldChangesElementInfo = mElementToInfoMap.get(childElement); if (oldChangesElementInfo != null && oldChangesElementInfo.parentElement == element) { setElementParent(childElement, null); } } releaseNotNewChildrenHashSet(notNewChildrenSet); } private void setElementParent(Object element, Object parentElement) { ElementInfo changesElementInfo = mElementToInfoChangesMap.get(element); if (changesElementInfo != null && parentElement == changesElementInfo.parentElement) { return; } ElementInfo oldElementInfo = mElementToInfoMap.get(element); if (changesElementInfo == null && oldElementInfo != null && parentElement == oldElementInfo.parentElement) { return; } if (changesElementInfo != null && oldElementInfo != null && parentElement == oldElementInfo.parentElement && ListUtil.identityEquals(oldElementInfo.children, changesElementInfo.children)) { mElementToInfoChangesMap.remove(element); if (parentElement == null) { mRootElementChanges.remove(element); } return; } List children = (changesElementInfo != null) ? changesElementInfo.children : (oldElementInfo != null) ? oldElementInfo.children : Collections.emptyList(); ElementInfo newElementInfo = new ElementInfo(element, parentElement, children); mElementToInfoChangesMap.put(element, newElementInfo); if (parentElement == null) { mRootElementChanges.add(element); } else { mRootElementChanges.remove(element); } } public Update build() { return new Update(mElementToInfoChangesMap, mRootElementChanges); } private HashSet acquireNotNewChildrenHashSet() { HashSet notNewChildrenHashSet = mCachedNotNewChildrenSet; if (notNewChildrenHashSet == null) { notNewChildrenHashSet = new HashSet<>(); } mCachedNotNewChildrenSet = null; return notNewChildrenHashSet; } private void releaseNotNewChildrenHashSet(HashSet notNewChildrenHashSet) { notNewChildrenHashSet.clear(); if (mCachedNotNewChildrenSet == null) { mCachedNotNewChildrenSet = notNewChildrenHashSet; } } } public final class Update implements DocumentView { private final Map mElementToInfoChangesMap; private final Set mRootElementChangesSet; public Update( Map elementToInfoChangesMap, Set rootElementChangesSet) { mElementToInfoChangesMap = elementToInfoChangesMap; mRootElementChangesSet = rootElementChangesSet; } public boolean isEmpty() { return mElementToInfoChangesMap.isEmpty(); } public boolean isElementChanged(Object element) { return mElementToInfoChangesMap.containsKey(element); } public Object getRootElement() { return ShadowDocument.this.getRootElement(); } public ElementInfo getElementInfo(Object element) { // Return ElementInfo for the new (albeit uncommitted and pre-garbage collected) view of the // Document. If element is garbage then you'll still get its info (feature, not a bug :)). ElementInfo elementInfo = mElementToInfoChangesMap.get(element); if (elementInfo != null) { return elementInfo; } return mElementToInfoMap.get(element); } public void getChangedElements(Accumulator accumulator) { for (Object element : mElementToInfoChangesMap.keySet()) { accumulator.store(element); } } public void getGarbageElements(Accumulator accumulator) { // This queue stores pairs of elements, [element, expectedParent] // When we dequeue, we look at element's parentElement in the new view to see if it matches // expectedParent. If it does, then it's garbage. For enqueueing roots, whose parents are // null, since we can't enqueue null we instead enqueue the element twice. Queue queue = new ArrayDeque<>(); // Initialize the queue with all disconnected tree roots (parentElement == null) which // aren't the DOM root. for (Object element : mRootElementChangesSet) { ElementInfo newElementInfo = getElementInfo(element); if (element != mRootElement && newElementInfo.parentElement == null) { queue.add(element); queue.add(element); } } // BFS traversal from those elements in the old view of the tree and test each element // to see if it's still within a disconnected sub-tree. We can tell if it's garbage if its // parent element in the new view of the tree hasn't changed. while (!queue.isEmpty()) { final Object element = queue.remove(); final Object expectedParent0 = queue.remove(); final Object expectedParent = (element == expectedParent0) ? null : expectedParent0; final ElementInfo newElementInfo = getElementInfo(element); if (newElementInfo.parentElement == expectedParent) { accumulator.store(element); ElementInfo oldElementInfo = ShadowDocument.this.getElementInfo(element); if (oldElementInfo != null) { for (int i = 0, N = oldElementInfo.children.size(); i < N; ++i) { queue.add(oldElementInfo.children.get(i)); queue.add(element); } } } } } public void abandon() { if (!mIsUpdating) { throw new IllegalStateException(); } mIsUpdating = false; } public void commit() { if (!mIsUpdating) { throw new IllegalStateException(); } // Apply the changes to the tree mElementToInfoMap.putAll(mElementToInfoChangesMap); // Remove garbage elements: those that have a null parent (other than mRootElement), and // their entire sub-trees, but excluding reparented elements. for (Object element : mRootElementChangesSet) { removeGarbageSubTree(mElementToInfoMap, element); } mIsUpdating = false; // Not usually enabled because it's expensive. Very useful for debugging. //validateTree(mElementToInfoMap); } private void removeGarbageSubTree( Map elementToInfoMap, Object element) { final ElementInfo elementInfo = elementToInfoMap.get(element); // If this element has a parent (it's not a root), and that parent is still in the tree after // changes have been applied and after our caller (removeGarbageSubTree) removed another // element that claims this element as its child, then that means this element should not be // removed. It has been reparented, and recursion stops here. if (elementInfo.parentElement != null && elementToInfoMap.containsKey(elementInfo.parentElement)) { return; } elementToInfoMap.remove(element); for (int i = 0, N = elementInfo.children.size(); i < N; ++i) { removeGarbageSubTree(elementToInfoMap, elementInfo.children.get(i)); } } // This method is intended for use during debugging. Put a breakpoint on each throw statement in // order to catch structural problems in the tree. This method should only be called at the very // end of commit(). private void validateTree(Map elementToInfoMap) { // We need a tree, not a forest. HashSet rootElements = new HashSet<>(); for (Map.Entry entry : elementToInfoMap.entrySet()) { final Object element = entry.getKey(); final ElementInfo elementInfo = entry.getValue(); if (element != elementInfo.element) { // should not be possible throw new IllegalStateException("element != elementInfo.element"); } // Verify children for (int i = 0, N = elementInfo.children.size(); i < N; ++i) { final Object childElement = elementInfo.children.get(i); final ElementInfo childElementInfo = elementToInfoMap.get(childElement); if (childElementInfo == null) { throw new IllegalStateException(String.format( "elementInfo.get(elementInfo.children.get(%s)) == null", i)); } if (childElementInfo.parentElement != element) { throw new IllegalStateException("childElementInfo.parentElement != element"); } } // Verify parent if (elementInfo.parentElement == null) { rootElements.add(element); } else { final ElementInfo parentElementInfo = elementToInfoMap.get(elementInfo.parentElement); if (parentElementInfo == null) { throw new IllegalStateException( "elementToInfoMap.get(elementInfo.parentElementInfo) == NULL"); } if (elementInfo.parentElement != parentElementInfo.element) { // should not be possible throw new IllegalStateException( "elementInfo.parentElementInfo != parentElementInfo.parent"); } if (!parentElementInfo.children.contains(element)) { throw new IllegalStateException( "parentElementInfo.children.contains(element) == FALSE"); } } } if (rootElements.size() != 1) { throw new IllegalStateException( "elementToInfoMap is a forest, not a tree. rootElements.size() != 1"); } } } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/elements/StyleAccumulator.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.elements; public interface StyleAccumulator { void store(String name, String value, boolean isDefault); } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/elements/StyleRuleNameAccumulator.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.elements; public interface StyleRuleNameAccumulator { void store(String ruleName, boolean editable); } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/elements/android/AccessibilityNodeInfoWrapper.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.elements.android; import android.text.TextUtils; import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; import android.widget.EditText; import com.facebook.stetho.common.android.AccessibilityUtil; import androidx.annotation.Nullable; import androidx.core.view.ViewCompat; import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat; public final class AccessibilityNodeInfoWrapper { public AccessibilityNodeInfoWrapper() { } public static AccessibilityNodeInfoCompat createNodeInfoFromView(View view) { AccessibilityNodeInfoCompat nodeInfo = AccessibilityNodeInfoCompat.obtain(); ViewCompat.onInitializeAccessibilityNodeInfo(view, nodeInfo); return nodeInfo; } public static boolean getIsAccessibilityFocused(View view) { AccessibilityNodeInfoCompat node = createNodeInfoFromView(view); boolean isAccessibilityFocused = node.isAccessibilityFocused(); node.recycle(); return isAccessibilityFocused; } public static boolean getIgnored(View view) { int important = ViewCompat.getImportantForAccessibility(view); if (important == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO || important == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS) { return true; } // Go all the way up the tree to make sure no parent has hidden its descendants ViewParent parent = view.getParent(); while (parent instanceof View) { if (ViewCompat.getImportantForAccessibility((View) parent) == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS) { return true; } parent = parent.getParent(); } AccessibilityNodeInfoCompat node = createNodeInfoFromView(view); try { if (!node.isVisibleToUser()) { return true; } if (AccessibilityUtil.isAccessibilityFocusable(node, view)) { if (node.getChildCount() <= 0) { // Leaves that are accessibility focusable are never ignored, even if they don't have a // speakable description return false; } else if (AccessibilityUtil.isSpeakingNode(node, view)) { // Node is focusable and has something to speak return false; } // Node is focusable and has nothing to speak return true; } // If this node has no focusable ancestors, but it still has text, // then it should receive focus from navigation and be read aloud. if (!AccessibilityUtil.hasFocusableAncestor(node, view) && AccessibilityUtil.hasText(node)) { return false; } return true; } finally { node.recycle(); } } public static String getIgnoredReasons(View view) { int important = ViewCompat.getImportantForAccessibility(view); if (important == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO) { return "View has importantForAccessibility set to 'NO'."; } if (important == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS) { return "View has importantForAccessibility set to 'NO_HIDE_DESCENDANTS'."; } ViewParent parent = view.getParent(); while (parent instanceof View) { if (ViewCompat.getImportantForAccessibility((View) parent) == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS) { return "An ancestor View has importantForAccessibility set to 'NO_HIDE_DESCENDANTS'."; } parent = parent.getParent(); } AccessibilityNodeInfoCompat node = createNodeInfoFromView(view); try { if (!node.isVisibleToUser()) { return "View is not visible."; } if (AccessibilityUtil.isAccessibilityFocusable(node, view)) { return "View is actionable, but has no description."; } if (AccessibilityUtil.hasText(node)) { return "View is not actionable, and an ancestor View has co-opted its description."; } return "View is not actionable and has no description."; } finally { node.recycle(); } } @Nullable public static String getFocusableReasons(View view) { AccessibilityNodeInfoCompat node = createNodeInfoFromView(view); try { boolean hasText = AccessibilityUtil.hasText(node); boolean isCheckable = node.isCheckable(); boolean hasNonActionableSpeakingDescendants = AccessibilityUtil.hasNonActionableSpeakingDescendants(node, view); if (AccessibilityUtil.isActionableForAccessibility(node)) { if (node.getChildCount() <= 0) { return "View is actionable and has no children."; } else if (hasText) { return "View is actionable and has a description."; } else if (isCheckable) { return "View is actionable and checkable."; } else if (hasNonActionableSpeakingDescendants) { return "View is actionable and has non-actionable descendants with descriptions."; } } if (AccessibilityUtil.isTopLevelScrollItem(node, view)) { if (hasText) { return "View is a direct child of a scrollable container and has a description."; } else if (isCheckable) { return "View is a direct child of a scrollable container and is checkable."; } else if (hasNonActionableSpeakingDescendants) { return "View is a direct child of a scrollable container and has non-actionable " + "descendants with descriptions."; } } if (hasText) { return "View has a description and is not actionable, but has no actionable ancestor."; } return null; } finally { node.recycle(); } } @Nullable public static String getActions(View view) { AccessibilityNodeInfoCompat node = createNodeInfoFromView(view); try { final StringBuilder actionLabels = new StringBuilder(); final String separator = ", "; for (AccessibilityActionCompat action : node.getActionList()) { if (actionLabels.length() > 0) { actionLabels.append(separator); } switch (action.getId()) { case AccessibilityNodeInfoCompat.ACTION_FOCUS: actionLabels.append("focus"); break; case AccessibilityNodeInfoCompat.ACTION_CLEAR_FOCUS: actionLabels.append("clear-focus"); break; case AccessibilityNodeInfoCompat.ACTION_SELECT: actionLabels.append("select"); break; case AccessibilityNodeInfoCompat.ACTION_CLEAR_SELECTION: actionLabels.append("clear-selection"); break; case AccessibilityNodeInfoCompat.ACTION_CLICK: actionLabels.append("click"); break; case AccessibilityNodeInfoCompat.ACTION_LONG_CLICK: actionLabels.append("long-click"); break; case AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS: actionLabels.append("accessibility-focus"); break; case AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS: actionLabels.append("clear-accessibility-focus"); break; case AccessibilityNodeInfoCompat.ACTION_NEXT_AT_MOVEMENT_GRANULARITY: actionLabels.append("next-at-movement-granularity"); break; case AccessibilityNodeInfoCompat.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY: actionLabels.append("previous-at-movement-granularity"); break; case AccessibilityNodeInfoCompat.ACTION_NEXT_HTML_ELEMENT: actionLabels.append("next-html-element"); break; case AccessibilityNodeInfoCompat.ACTION_PREVIOUS_HTML_ELEMENT: actionLabels.append("previous-html-element"); break; case AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD: actionLabels.append("scroll-forward"); break; case AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD: actionLabels.append("scroll-backward"); break; case AccessibilityNodeInfoCompat.ACTION_CUT: actionLabels.append("cut"); break; case AccessibilityNodeInfoCompat.ACTION_COPY: actionLabels.append("copy"); break; case AccessibilityNodeInfoCompat.ACTION_PASTE: actionLabels.append("paste"); break; case AccessibilityNodeInfoCompat.ACTION_SET_SELECTION: actionLabels.append("set-selection"); break; default: CharSequence label = action.getLabel(); if (label != null) { actionLabels.append(label); } else { actionLabels.append("unknown"); } break; } } return actionLabels.length() > 0 ? actionLabels.toString() : null; } finally { node.recycle(); } } @Nullable public static CharSequence getDescription(View view) { AccessibilityNodeInfoCompat node = createNodeInfoFromView(view); try { CharSequence contentDescription = node.getContentDescription(); CharSequence nodeText = node.getText(); boolean hasNodeText = !TextUtils.isEmpty(nodeText); boolean isEditText = view instanceof EditText; // EditText's prioritize their own text content over a contentDescription if (!TextUtils.isEmpty(contentDescription) && (!isEditText || !hasNodeText)) { return contentDescription; } if (hasNodeText) { return nodeText; } // If there are child views and no contentDescription the text of all non-focusable children, // comma separated, becomes the description. if (view instanceof ViewGroup) { final StringBuilder concatChildDescription = new StringBuilder(); final String separator = ", "; ViewGroup viewGroup = (ViewGroup) view; for (int i = 0, count = viewGroup.getChildCount(); i < count; i++) { final View child = viewGroup.getChildAt(i); AccessibilityNodeInfoCompat childNodeInfo = AccessibilityNodeInfoCompat.obtain(); ViewCompat.onInitializeAccessibilityNodeInfo(child, childNodeInfo); CharSequence childNodeDescription = null; if (AccessibilityUtil.isSpeakingNode(childNodeInfo, child) && !AccessibilityUtil.isAccessibilityFocusable(childNodeInfo, child)) { childNodeDescription = getDescription(child); } if (!TextUtils.isEmpty(childNodeDescription)) { if (concatChildDescription.length() > 0) { concatChildDescription.append(separator); } concatChildDescription.append(childNodeDescription); } childNodeInfo.recycle(); } return concatChildDescription.length() > 0 ? concatChildDescription.toString() : null; } return null; } finally { node.recycle(); } } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/elements/android/ActivityDescriptor.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.elements.android; import android.app.Activity; import android.graphics.Rect; import android.view.View; import android.view.Window; import com.facebook.stetho.common.Accumulator; import com.facebook.stetho.common.StringUtil; import com.facebook.stetho.common.android.FragmentActivityAccessor; import com.facebook.stetho.common.android.FragmentCompat; import com.facebook.stetho.common.android.FragmentManagerAccessor; import com.facebook.stetho.inspector.elements.AbstractChainedDescriptor; import com.facebook.stetho.inspector.elements.Descriptor; import javax.annotation.Nullable; import java.util.List; final class ActivityDescriptor extends AbstractChainedDescriptor implements HighlightableDescriptor { @Override protected String onGetNodeName(Activity element) { String className = element.getClass().getName(); return StringUtil.removePrefix(className, "android.app."); } @Override protected void onGetChildren(Activity element, Accumulator children) { getDialogFragments(FragmentCompat.getSupportLibInstance(), element, children); getDialogFragments(FragmentCompat.getFrameworkInstance(), element, children); Window window = element.getWindow(); if (window != null) { children.store(window); } } @Nullable @Override public View getViewAndBoundsForHighlighting(Activity element, Rect bounds) { final Descriptor.Host host = getHost(); Window window = null; HighlightableDescriptor descriptor = null; if (host instanceof AndroidDescriptorHost) { window = element.getWindow(); descriptor = ((AndroidDescriptorHost) host).getHighlightableDescriptor(window); } return descriptor == null ? null : descriptor.getViewAndBoundsForHighlighting(window, bounds); } @Nullable @Override public Object getElementToHighlightAtPosition(Activity element, int x, int y, Rect bounds) { final Descriptor.Host host = getHost(); Window window = null; HighlightableDescriptor descriptor = null; if (host instanceof AndroidDescriptorHost) { window = element.getWindow(); descriptor = ((AndroidDescriptorHost) host).getHighlightableDescriptor(window); } return descriptor == null ? null : descriptor.getElementToHighlightAtPosition(window, x, y, bounds); } private static void getDialogFragments( @Nullable FragmentCompat compat, Activity activity, Accumulator accumulator) { if (compat == null || !compat.getFragmentActivityClass().isInstance(activity)) { return; } FragmentActivityAccessor activityAccessor = compat.forFragmentActivity(); Object fragmentManager = activityAccessor.getFragmentManager(activity); if (fragmentManager == null) { return; } FragmentManagerAccessor fragmentManagerAccessor = compat.forFragmentManager(); List addedFragments = fragmentManagerAccessor.getAddedFragments(fragmentManager); if (addedFragments == null) { return; } for (int i = 0, N = addedFragments.size(); i < N; ++i) { final Object fragment = addedFragments.get(i); if (compat.getDialogFragmentClass().isInstance(fragment)) { accumulator.store(fragment); } } } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/elements/android/ActivityTracker.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.elements.android; import android.annotation.TargetApi; import android.app.Activity; import android.app.Application; import android.os.Build; import android.os.Bundle; import android.os.Looper; import androidx.annotation.NonNull; import com.facebook.stetho.common.Util; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import javax.annotation.Nullable; import javax.annotation.concurrent.GuardedBy; import javax.annotation.concurrent.NotThreadSafe; /** * Tracks which {@link Activity} instances have been created and not yet destroyed in creation * order for use by Stetho features. Note that automatic tracking is not available for * all versions of Android but it is possible to manually track activities using the {@link #add} * and {@link #remove} methods exposed below. Be aware that this is an easy opportunity to * cause serious memory leaks in your application however. Use with caution. *

* Most callers can and should ignore this class, though it is necessary if you are implementing * Activity tracking pre-ICS. */ @NotThreadSafe public final class ActivityTracker { private static final ActivityTracker sInstance = new ActivityTracker(); /** * Use {@link WeakReference} here to silence a false positive from LeakCanary: * https://github.com/facebook/stetho/issues/319#issuecomment-285699813 */ @GuardedBy("Looper.getMainLooper()") private final ArrayList> mActivities = new ArrayList<>(); private final List> mActivitiesUnmodifiable = Collections.unmodifiableList(mActivities); private final List mListeners = new CopyOnWriteArrayList<>(); @Nullable private AutomaticTracker mAutomaticTracker; public static ActivityTracker get() { return sInstance; } public void registerListener(Listener listener) { mListeners.add(listener); } public void unregisterListener(Listener listener) { mListeners.remove(listener); } /** * Start automatic tracking if we are running on ICS+. * * @return Automatic tracking has been started. No need to manually invoke {@link #add} or * {@link #remove} methods. */ public boolean beginTrackingIfPossible(Application application) { if (mAutomaticTracker == null) { AutomaticTracker automaticTracker = AutomaticTracker.newInstance(application, this /* tracker */); automaticTracker.register(); mAutomaticTracker = automaticTracker; return true; } return false; } public boolean endTracking() { if (mAutomaticTracker != null) { mAutomaticTracker.unregister(); mAutomaticTracker = null; return true; } return false; } public void add(Activity activity) { Util.throwIfNull(activity); Util.throwIfNot(Looper.myLooper() == Looper.getMainLooper()); mActivities.add(new WeakReference<>(activity)); for (Listener listener : mListeners) { listener.onActivityAdded(activity); } } public void remove(Activity activity) { Util.throwIfNull(activity); Util.throwIfNot(Looper.myLooper() == Looper.getMainLooper()); if (removeFromWeakList(mActivities, activity)) { for (Listener listener : mListeners) { listener.onActivityRemoved(activity); } } } private static boolean removeFromWeakList(ArrayList> haystack, T needle) { for (int i = 0, N = haystack.size(); i < N; i++) { T hay = haystack.get(i).get(); if (hay == needle) { haystack.remove(i); return true; } } return false; } public List> getActivitiesView() { return mActivitiesUnmodifiable; } @Nullable public Activity tryGetTopActivity() { if (mActivitiesUnmodifiable.isEmpty()) { return null; } for (int i = mActivitiesUnmodifiable.size() - 1; i >= 0; i--) { Activity activity = mActivitiesUnmodifiable.get(i).get(); if (activity != null) { return activity; } } return null; } public interface Listener { public void onActivityAdded(Activity activity); public void onActivityRemoved(Activity activity); } private static abstract class AutomaticTracker { @NonNull public static AutomaticTracker newInstance( Application application, ActivityTracker tracker) { return new AutomaticTrackerICSAndBeyond(application, tracker); } public abstract void register(); public abstract void unregister(); @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) private static class AutomaticTrackerICSAndBeyond extends AutomaticTracker { private final Application mApplication; private final ActivityTracker mTracker; public AutomaticTrackerICSAndBeyond(Application application, ActivityTracker tracker) { mApplication = application; mTracker = tracker; } public void register() { mApplication.registerActivityLifecycleCallbacks(mLifecycleCallbacks); } public void unregister() { mApplication.unregisterActivityLifecycleCallbacks(mLifecycleCallbacks); } private final Application.ActivityLifecycleCallbacks mLifecycleCallbacks = new Application.ActivityLifecycleCallbacks() { @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) { mTracker.add(activity); } @Override public void onActivityStarted(Activity activity) { } @Override public void onActivityResumed(Activity activity) { } @Override public void onActivityPaused(Activity activity) { } @Override public void onActivityStopped(Activity activity) { } @Override public void onActivitySaveInstanceState(Activity activity, Bundle outState) { } @Override public void onActivityDestroyed(Activity activity) { mTracker.remove(activity); } }; } } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/elements/android/AndroidDescriptorHost.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.elements.android; import com.facebook.stetho.inspector.elements.Descriptor; import javax.annotation.Nullable; interface AndroidDescriptorHost extends Descriptor.Host { @Nullable HighlightableDescriptor getHighlightableDescriptor(@Nullable Object element); } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/elements/android/AndroidDocumentConstants.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.elements.android; import android.os.Build; public interface AndroidDocumentConstants { /** * Minimum API version required to make effective use of AndroidDocumentProvider. This can be * moved back significantly through manual APIs to discover {@link android.app.Activity} * instances. */ int MIN_API_LEVEL = Build.VERSION_CODES.ICE_CREAM_SANDWICH; } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/elements/android/AndroidDocumentProvider.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.elements.android; import android.app.Activity; import android.app.Application; import android.app.Dialog; import android.content.Context; import android.graphics.Canvas; import android.graphics.Rect; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.Window; import android.view.WindowManager; import android.widget.TextView; import com.facebook.stetho.common.Accumulator; import com.facebook.stetho.common.Predicate; import com.facebook.stetho.common.ThreadBound; import com.facebook.stetho.common.Util; import com.facebook.stetho.inspector.elements.DocumentProvider; import com.facebook.stetho.inspector.elements.Descriptor; import com.facebook.stetho.inspector.elements.DescriptorProvider; import com.facebook.stetho.inspector.elements.DescriptorMap; import com.facebook.stetho.inspector.elements.DocumentProviderListener; import com.facebook.stetho.inspector.elements.NodeDescriptor; import com.facebook.stetho.inspector.elements.ObjectDescriptor; import com.facebook.stetho.inspector.helper.ThreadBoundProxy; import javax.annotation.Nullable; import java.util.ArrayList; import java.util.List; final class AndroidDocumentProvider extends ThreadBoundProxy implements DocumentProvider, AndroidDescriptorHost { private static final int INSPECT_OVERLAY_COLOR = 0x40FFFFFF; private static final int INSPECT_HOVER_COLOR = 0x404040ff; private final Rect mHighlightingBoundsRect = new Rect(); private final Rect mHitRect = new Rect(); private final Application mApplication; private final DescriptorMap mDescriptorMap; private final AndroidDocumentRoot mDocumentRoot; private final ViewHighlighter mHighlighter; private final InspectModeHandler mInspectModeHandler; private @Nullable DocumentProviderListener mListener; // We don't yet have an an implementation for reliably detecting fine-grained changes in the // View tree. So, for now at least, we have a timer that runs every so often and just reports // that we changed. Our listener will then read the entire Document from us and transmit the // changes to Chrome. Detecting, reporting, and traversing fine-grained changes is a future work // item (see Issue #210). private static final long REPORT_CHANGED_INTERVAL_MS = 1000; private boolean mIsReportChangesTimerPosted = false; private final Runnable mReportChangesTimer = new Runnable() { @Override public void run() { mIsReportChangesTimerPosted = false; if (mListener != null) { mListener.onPossiblyChanged(); mIsReportChangesTimerPosted = true; postDelayed(this, REPORT_CHANGED_INTERVAL_MS); } } }; public AndroidDocumentProvider( Application application, List descriptorProviders, ThreadBound enforcer) { super(enforcer); mApplication = Util.throwIfNull(application); mDocumentRoot = new AndroidDocumentRoot(application); mDescriptorMap = new DescriptorMap() .beginInit() .registerDescriptor(Activity.class, new ActivityDescriptor()) .registerDescriptor(AndroidDocumentRoot.class, mDocumentRoot) .registerDescriptor(Application.class, new ApplicationDescriptor()) .registerDescriptor(Dialog.class, new DialogDescriptor()) .registerDescriptor(Object.class, new ObjectDescriptor()) .registerDescriptor(TextView.class, new TextViewDescriptor()) .registerDescriptor(View.class, new ViewDescriptor()) .registerDescriptor(ViewGroup.class, new ViewGroupDescriptor()) .registerDescriptor(Window.class, new WindowDescriptor()); DialogFragmentDescriptor.register(mDescriptorMap); FragmentDescriptor.register(mDescriptorMap); for (int i = 0, size = descriptorProviders.size(); i < size; ++i) { final DescriptorProvider descriptorProvider = descriptorProviders.get(i); descriptorProvider.registerDescriptor(mDescriptorMap); } mDescriptorMap.setHost(this).endInit(); mHighlighter = ViewHighlighter.newInstance(); mInspectModeHandler = new InspectModeHandler(); } @Override public void dispose() { verifyThreadAccess(); mHighlighter.clearHighlight(); mInspectModeHandler.disable(); removeCallbacks(mReportChangesTimer); mIsReportChangesTimerPosted = false; mListener = null; } @Override public void setListener(DocumentProviderListener listener) { verifyThreadAccess(); mListener = listener; if (mListener == null && mIsReportChangesTimerPosted) { mIsReportChangesTimerPosted = false; removeCallbacks(mReportChangesTimer); } else if (mListener != null && !mIsReportChangesTimerPosted) { mIsReportChangesTimerPosted = true; postDelayed(mReportChangesTimer, REPORT_CHANGED_INTERVAL_MS); } } @Override public Object getRootElement() { verifyThreadAccess(); return mDocumentRoot; } @Override public NodeDescriptor getNodeDescriptor(Object element) { verifyThreadAccess(); return getDescriptor(element); } @Override public void highlightElement(Object element, int color) { verifyThreadAccess(); final HighlightableDescriptor descriptor = getHighlightableDescriptor(element); if (descriptor == null) { mHighlighter.clearHighlight(); return; } mHighlightingBoundsRect.setEmpty(); final View highlightingView = descriptor.getViewAndBoundsForHighlighting(element, mHighlightingBoundsRect); if (highlightingView == null) { mHighlighter.clearHighlight(); return; } mHighlighter.setHighlightedView( highlightingView, mHighlightingBoundsRect, color); } @Override public void hideHighlight() { verifyThreadAccess(); mHighlighter.clearHighlight(); } @Override public void setInspectModeEnabled(boolean enabled) { verifyThreadAccess(); if (enabled) { mInspectModeHandler.enable(); } else { mInspectModeHandler.disable(); } } @Override public void setAttributesAsText(Object element, String text) { verifyThreadAccess(); Descriptor descriptor = mDescriptorMap.get(element.getClass()); if (descriptor != null) { descriptor.setAttributesAsText(element, text); } } // Descriptor.Host implementation @Override public Descriptor getDescriptor(Object element) { return (element == null) ? null : mDescriptorMap.get(element.getClass()); } @Override public void onAttributeModified(Object element, String name, String value) { if (mListener != null) { mListener.onAttributeModified(element, name, value); } } @Override public void onAttributeRemoved(Object element, String name) { if (mListener != null) { mListener.onAttributeRemoved(element, name); } } // AndroidDescriptorHost implementation @Override @Nullable public HighlightableDescriptor getHighlightableDescriptor(@Nullable Object element) { if (element == null) { return null; } HighlightableDescriptor highlightableDescriptor = null; Class theClass = element.getClass(); Descriptor lastDescriptor = null; while (highlightableDescriptor == null && theClass != null) { Descriptor descriptor = mDescriptorMap.get(theClass); if (descriptor == null) { return null; } if (descriptor != lastDescriptor && descriptor instanceof HighlightableDescriptor) { highlightableDescriptor = ((HighlightableDescriptor) descriptor); } lastDescriptor = descriptor; theClass = theClass.getSuperclass(); } return highlightableDescriptor; } private void getWindows(final Accumulator accumulator) { Descriptor appDescriptor = getDescriptor(mApplication); if (appDescriptor != null) { Accumulator elementAccumulator = new Accumulator() { @Override public void store(Object element) { if (element instanceof Window) { // Store the Window and do not recurse into its children. accumulator.store((Window) element); } else { // Recursively scan this element's children in search of more Windows. Descriptor elementDescriptor = getDescriptor(element); if (elementDescriptor != null) { elementDescriptor.getChildren(element, this); } } } }; appDescriptor.getChildren(mApplication, elementAccumulator); } } private final class InspectModeHandler { private final Predicate mViewSelector = new Predicate() { @Override public boolean apply(View view) { return !(view instanceof DocumentHiddenView); } }; private List mOverlays; public void enable() { verifyThreadAccess(); if (mOverlays != null) { disable(); } mOverlays = new ArrayList<>(); getWindows(new Accumulator() { @Override public void store(Window object) { if (object.peekDecorView() instanceof ViewGroup) { final ViewGroup decorView = (ViewGroup) object.peekDecorView(); OverlayView overlayView = new OverlayView(mApplication); WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(); layoutParams.width = WindowManager.LayoutParams.MATCH_PARENT; layoutParams.height = WindowManager.LayoutParams.MATCH_PARENT; decorView.addView(overlayView, layoutParams); decorView.bringChildToFront(overlayView); mOverlays.add(overlayView); } } }); } public void disable() { verifyThreadAccess(); if (mOverlays == null) { return; } for (int i = 0; i < mOverlays.size(); ++i) { final View overlayView = mOverlays.get(i); ViewGroup decorViewGroup = (ViewGroup)overlayView.getParent(); decorViewGroup.removeView(overlayView); } mOverlays = null; } private final class OverlayView extends DocumentHiddenView { public OverlayView(Context context) { super(context); } @Override protected void onDraw(Canvas canvas) { canvas.drawColor(INSPECT_OVERLAY_COLOR); super.onDraw(canvas); } @Override public boolean onTouchEvent(MotionEvent event) { int x = (int) event.getX(); int y = (int) event.getY(); Object elementToHighlight = getParent(); while (true) { final HighlightableDescriptor descriptor = getHighlightableDescriptor(elementToHighlight); if (descriptor == null) { break; } mHitRect.setEmpty(); final Object element = descriptor.getElementToHighlightAtPosition(elementToHighlight, x, y, mHitRect); x -= mHitRect.left; y -= mHitRect.top; if (element == elementToHighlight) { break; } elementToHighlight = element; } if (elementToHighlight != null) { final HighlightableDescriptor descriptor = getHighlightableDescriptor(elementToHighlight); if (descriptor != null) { final View viewToHighlight = descriptor.getViewAndBoundsForHighlighting( elementToHighlight, mHighlightingBoundsRect); if (event.getAction() != MotionEvent.ACTION_CANCEL) { if (viewToHighlight != null) { mHighlighter.setHighlightedView( viewToHighlight, mHighlightingBoundsRect, INSPECT_HOVER_COLOR); if (event.getAction() == MotionEvent.ACTION_UP) { if (mListener != null) { mListener.onInspectRequested(elementToHighlight); } } } } } } return true; } } } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/elements/android/AndroidDocumentProviderFactory.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.elements.android; import android.app.Application; import android.os.Handler; import android.os.Looper; import com.facebook.stetho.common.ThreadBound; import com.facebook.stetho.common.UncheckedCallable; import com.facebook.stetho.common.Util; import com.facebook.stetho.common.android.HandlerUtil; import com.facebook.stetho.inspector.elements.DescriptorProvider; import com.facebook.stetho.inspector.elements.DocumentProvider; import com.facebook.stetho.inspector.elements.DocumentProviderFactory; import java.util.List; public final class AndroidDocumentProviderFactory implements DocumentProviderFactory, ThreadBound { private final Application mApplication; private final List mDescriptorProviders; private final Handler mHandler; public AndroidDocumentProviderFactory( Application application, List descriptorProviders) { mApplication = Util.throwIfNull(application); mDescriptorProviders = Util.throwIfNull(descriptorProviders); mHandler = new Handler(Looper.getMainLooper()); } @Override public DocumentProvider create() { return new AndroidDocumentProvider(mApplication, mDescriptorProviders, this); } // ThreadBound implementation @Override public boolean checkThreadAccess() { return HandlerUtil.checkThreadAccess(mHandler); } @Override public void verifyThreadAccess() { HandlerUtil.verifyThreadAccess(mHandler); } @Override public V postAndWait(UncheckedCallable c) { return HandlerUtil.postAndWait(mHandler, c); } @Override public void postAndWait(Runnable r) { HandlerUtil.postAndWait(mHandler, r); } @Override public void postDelayed(Runnable r, long delayMillis) { if (!mHandler.postDelayed(r, delayMillis)) { throw new RuntimeException("Handler.postDelayed() returned false"); } } @Override public void removeCallbacks(Runnable r) { mHandler.removeCallbacks(r); } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/elements/android/AndroidDocumentRoot.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.elements.android; import android.app.Application; import com.facebook.stetho.common.Accumulator; import com.facebook.stetho.common.Util; import com.facebook.stetho.inspector.elements.AbstractChainedDescriptor; import com.facebook.stetho.inspector.elements.NodeType; // For the root, we use 1 object for both element and descriptor. final class AndroidDocumentRoot extends AbstractChainedDescriptor { private final Application mApplication; public AndroidDocumentRoot(Application application) { mApplication = Util.throwIfNull(application); } @Override protected NodeType onGetNodeType(AndroidDocumentRoot element) { return NodeType.DOCUMENT_NODE; } @Override protected String onGetNodeName(AndroidDocumentRoot element) { return "root"; } @Override protected void onGetChildren(AndroidDocumentRoot element, Accumulator children) { children.store(mApplication); } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/elements/android/ApplicationDescriptor.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.elements.android; import android.app.Activity; import android.app.Application; import android.view.View; import com.facebook.stetho.common.Accumulator; import com.facebook.stetho.common.Util; import com.facebook.stetho.inspector.elements.AbstractChainedDescriptor; import com.facebook.stetho.inspector.elements.NodeType; import com.facebook.stetho.inspector.elements.android.window.WindowRootViewCompat; import java.lang.ref.WeakReference; import java.util.Collections; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; final class ApplicationDescriptor extends AbstractChainedDescriptor { private final Map mElementToContextMap = Collections.synchronizedMap(new IdentityHashMap()); private final ActivityTracker mActivityTracker = ActivityTracker.get(); private ElementContext getContext(Application element) { return mElementToContextMap.get(element); } @Override protected void onHook(Application element) { ElementContext context = new ElementContext(); context.hook(element); mElementToContextMap.put(element, context); } @Override protected void onUnhook(Application element) { ElementContext context = mElementToContextMap.remove(element); context.unhook(); } @Override protected NodeType onGetNodeType(Application element) { return NodeType.ELEMENT_NODE; } @Override protected void onGetChildren(Application element, Accumulator children) { ElementContext context = getContext(element); List> activities = context.getActivitiesList(); // We report these in reverse order so that the newer ones show up on top for (int i = activities.size() - 1; i >= 0; --i) { Activity activity = activities.get(i).get(); if (activity != null) { children.store(activity); } } storeWindowIfNeeded(element, children, activities); } private void storeWindowIfNeeded(Application application, Accumulator children, List> activities) { List rootViews = WindowRootViewCompat.get(application).getRootViews(); for (View view : rootViews) { if (!isDecorViewOfActivity(view, activities)) { children.store(view); } } } private static boolean isDecorViewOfActivity(View view, List> references) { Util.throwIfNull(references); for (WeakReference reference : references) { Activity activity = reference.get(); if (activity == null) { continue; } if (activity.getWindow().getDecorView() == view) { return true; } } return false; } private class ElementContext { private Application mElement; public ElementContext() { } public void hook(Application element) { mElement = element; mActivityTracker.registerListener(mListener); } public void unhook() { mActivityTracker.unregisterListener(mListener); mElement = null; } public List> getActivitiesList() { return mActivityTracker.getActivitiesView(); } private final ActivityTracker.Listener mListener = new ActivityTracker.Listener() { @Override public void onActivityAdded(Activity activity) { // TODO: once we have the ability to report fine-grained updates, do that here } @Override public void onActivityRemoved(Activity activity) { // TODO: once we have the ability to report fine-grained updates, do that here } }; } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/elements/android/DialogDescriptor.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.elements.android; import android.app.Dialog; import android.graphics.Rect; import android.view.View; import android.view.Window; import com.facebook.stetho.common.Accumulator; import com.facebook.stetho.inspector.elements.AbstractChainedDescriptor; import com.facebook.stetho.inspector.elements.Descriptor; import javax.annotation.Nullable; final class DialogDescriptor extends AbstractChainedDescriptor implements HighlightableDescriptor { @Override protected void onGetChildren(Dialog element, Accumulator children) { Window window = element.getWindow(); if (window != null) { children.store(window); } } @Nullable @Override public View getViewAndBoundsForHighlighting(Dialog element, Rect bounds) { final Descriptor.Host host = getHost(); Window window = null; HighlightableDescriptor descriptor = null; if (host instanceof AndroidDescriptorHost) { window = element.getWindow(); descriptor = ((AndroidDescriptorHost) host).getHighlightableDescriptor(window); } return descriptor == null ? null : descriptor.getViewAndBoundsForHighlighting(window, bounds); } @Nullable @Override public Object getElementToHighlightAtPosition(Dialog element, int x, int y, Rect bounds) { final Descriptor.Host host = getHost(); Window window = null; HighlightableDescriptor descriptor = null; if (host instanceof AndroidDescriptorHost) { window = element.getWindow(); descriptor = ((AndroidDescriptorHost) host).getHighlightableDescriptor(window); } return descriptor == null ? null : descriptor.getElementToHighlightAtPosition(window, x, y, bounds); } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/elements/android/DialogFragmentDescriptor.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.elements.android; import android.app.Dialog; import android.graphics.Rect; import android.view.View; import com.facebook.stetho.common.Accumulator; import com.facebook.stetho.common.LogUtil; import com.facebook.stetho.common.Util; import com.facebook.stetho.common.android.DialogFragmentAccessor; import com.facebook.stetho.common.android.FragmentCompat; import com.facebook.stetho.inspector.elements.AbstractChainedDescriptor; import com.facebook.stetho.inspector.elements.AttributeAccumulator; import com.facebook.stetho.inspector.elements.ChainedDescriptor; import com.facebook.stetho.inspector.elements.ComputedStyleAccumulator; import com.facebook.stetho.inspector.elements.Descriptor; import com.facebook.stetho.inspector.elements.DescriptorMap; import com.facebook.stetho.inspector.elements.NodeType; import com.facebook.stetho.inspector.elements.StyleAccumulator; import com.facebook.stetho.inspector.elements.StyleRuleNameAccumulator; import javax.annotation.Nullable; final class DialogFragmentDescriptor extends Descriptor implements ChainedDescriptor, HighlightableDescriptor { private final DialogFragmentAccessor mAccessor; private Descriptor mSuper; public static DescriptorMap register(DescriptorMap map) { maybeRegister(map, FragmentCompat.getSupportLibInstance()); maybeRegister(map, FragmentCompat.getFrameworkInstance()); return map; } private static void maybeRegister(DescriptorMap map, @Nullable FragmentCompat compat) { if (compat != null) { Class dialogFragmentClass = compat.getDialogFragmentClass(); LogUtil.d("Adding support for %s", dialogFragmentClass); map.registerDescriptor(dialogFragmentClass, new DialogFragmentDescriptor(compat)); } } private DialogFragmentDescriptor(FragmentCompat compat) { mAccessor = compat.forDialogFragment(); } @Override public void setSuper(Descriptor superDescriptor) { Util.throwIfNull(superDescriptor); if (superDescriptor != mSuper) { if (mSuper != null) { throw new IllegalStateException(); } mSuper = superDescriptor; } } @Override public void hook(Object element) { mSuper.hook(element); } @Override public void unhook(Object element) { mSuper.unhook(element); } @Override public NodeType getNodeType(Object element) { return mSuper.getNodeType(element); } @Override public String getNodeName(Object element) { return mSuper.getNodeName(element); } @Override public String getLocalName(Object element) { return mSuper.getLocalName(element); } @Nullable @Override public String getNodeValue(Object element) { return mSuper.getNodeValue(element); } @Override public void getChildren(Object element, Accumulator children) { /** * We do NOT want the children from our super-{@link Descriptor}, which is probably * {@link FragmentDescriptor}. We only want to emit the {@link Dialog}, not the {@link View}. * Therefore, we don't call mSuper.getChildren(), and this is the reason why we don't derive * from {@link AbstractChainedDescriptor} (it doesn't allow a non-chained implementation of * {@link Descriptor#getChildren(Object, Accumulator)}). */ children.store(mAccessor.getDialog(element)); } @Override public void getAttributes(Object element, AttributeAccumulator attributes) { mSuper.getAttributes(element, attributes); } @Override public void setAttributesAsText(Object element, String text) { mSuper.setAttributesAsText(element, text); } @Nullable @Override public View getViewAndBoundsForHighlighting(Object element, Rect bounds) { final Descriptor.Host host = getHost(); Dialog dialog = null; HighlightableDescriptor descriptor = null; if (host instanceof AndroidDescriptorHost) { dialog = mAccessor.getDialog(element); descriptor = ((AndroidDescriptorHost) host).getHighlightableDescriptor(dialog); } return descriptor == null ? null : descriptor.getViewAndBoundsForHighlighting(dialog, bounds); } @Nullable @Override public Object getElementToHighlightAtPosition(Object element, int x, int y, Rect bounds) { final Descriptor.Host host = getHost(); Dialog dialog = null; HighlightableDescriptor descriptor = null; if (host instanceof AndroidDescriptorHost) { dialog = mAccessor.getDialog(element); descriptor = ((AndroidDescriptorHost) host).getHighlightableDescriptor(dialog); } return descriptor == null ? null : descriptor.getElementToHighlightAtPosition(dialog, x, y, bounds); } @Override public void getStyleRuleNames(Object element, StyleRuleNameAccumulator accumulator) { } @Override public void getStyles(Object element, String ruleName, StyleAccumulator accumulator) { } @Override public void setStyle(Object element, String ruleName, String name, String value) { } @Override public void getComputedStyles(Object element, ComputedStyleAccumulator styles) { } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/elements/android/DocumentHiddenView.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.elements.android; import android.content.Context; import android.view.View; class DocumentHiddenView extends View { public DocumentHiddenView(Context context) { super(context); } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/elements/android/FragmentDescriptor.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.elements.android; import android.graphics.Rect; import android.view.View; import com.facebook.stetho.common.Accumulator; import com.facebook.stetho.common.LogUtil; import com.facebook.stetho.common.android.FragmentAccessor; import com.facebook.stetho.common.android.FragmentCompat; import com.facebook.stetho.common.android.ResourcesUtil; import com.facebook.stetho.inspector.elements.AttributeAccumulator; import com.facebook.stetho.inspector.elements.AbstractChainedDescriptor; import com.facebook.stetho.inspector.elements.Descriptor; import com.facebook.stetho.inspector.elements.DescriptorMap; import javax.annotation.Nullable; final class FragmentDescriptor extends AbstractChainedDescriptor implements HighlightableDescriptor { private static final String ID_ATTRIBUTE_NAME = "id"; private static final String TAG_ATTRIBUTE_NAME = "tag"; private final FragmentAccessor mAccessor; public static DescriptorMap register(DescriptorMap map) { maybeRegister(map, FragmentCompat.getSupportLibInstance()); maybeRegister(map, FragmentCompat.getFrameworkInstance()); return map; } private static void maybeRegister(DescriptorMap map, @Nullable FragmentCompat compat) { if (compat != null) { Class fragmentClass = compat.getFragmentClass(); LogUtil.d("Adding support for %s", fragmentClass.getName()); map.registerDescriptor(fragmentClass, new FragmentDescriptor(compat)); } } private FragmentDescriptor(FragmentCompat compat) { mAccessor = compat.forFragment(); } @Override protected void onGetAttributes(Object element, AttributeAccumulator attributes) { int id = mAccessor.getId(element); if (id != FragmentAccessor.NO_ID) { String value = ResourcesUtil.getIdStringQuietly( element, mAccessor.getResources(element), id); attributes.store(ID_ATTRIBUTE_NAME, value); } String tag = mAccessor.getTag(element); if (tag != null && tag.length() > 0) { attributes.store(TAG_ATTRIBUTE_NAME, tag); } } @Override protected void onGetChildren(Object element, Accumulator children) { View view = mAccessor.getView(element); if (view != null) { children.store(view); } } @Override @Nullable public View getViewAndBoundsForHighlighting(Object element, Rect bounds) { return mAccessor.getView(element); } @Nullable @Override public Object getElementToHighlightAtPosition(Object element, int x, int y, Rect bounds) { final Descriptor.Host host = getHost(); View view = null; HighlightableDescriptor descriptor = null; if (host instanceof AndroidDescriptorHost) { view = mAccessor.getView(element); descriptor = ((AndroidDescriptorHost) host).getHighlightableDescriptor(view); } return descriptor == null ? null : descriptor.getElementToHighlightAtPosition(view, x, y, bounds); } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/elements/android/HighlightableDescriptor.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.elements.android; import android.graphics.Rect; import android.view.View; import javax.annotation.Nullable; public interface HighlightableDescriptor { /** * Return the {@link View} to highlight or null if this element cannot be highlighted. * If the element does not span the full bounds of the returned {@link View} you can set * the bounds of the passed in Rect. By default the passed in bounds are empty which means * highlight the full bounds of the {@link View}. */ @Nullable View getViewAndBoundsForHighlighting(E element, Rect bounds); /** * Used when activating find by touch feature to figure out which element to focus / highlight. * * @param element the element * @param y coordinate in local coordinate space * @param y coordinate in local coordinate space * @param bounds The bounds of the returned element. Used to offset the coordinates for next call. * @return A child element or self if this coordinate falls within self and not a child. */ @Nullable Object getElementToHighlightAtPosition(E element, int x, int y, Rect bounds); } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/elements/android/MethodInvoker.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.elements.android; import com.facebook.stetho.common.LogUtil; import com.facebook.stetho.common.Util; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Arrays; import java.util.List; /** * Tries to arbitrarily invoke single argument methods by name on an object instance by trying out * different argument types. */ public class MethodInvoker { private static final List> invokers = Arrays.asList( new StringMethodInvoker(), new CharSequenceMethodInvoker(), new IntegerMethodInvoker(), new FloatMethodInvoker(), new BooleanMethodInvoker()); /** * Tries to invoke a method on receiver with a single argument by trying out different types * for arg until it finds one that matches (or not). No exceptions are thrown on failure. * * @param methodName The method name to be invoked * @param argument The single argument to be provided to the method */ public void invoke(Object receiver, String methodName, String argument) { Util.throwIfNull(receiver, methodName, argument); int size = invokers.size(); for (int i = 0; i < size; ++i) { final TypedMethodInvoker invoker = invokers.get(i); if (invoker.invoke(receiver, methodName, argument)) { return; } } LogUtil.w("Method with name " + methodName + " not found for any of the MethodInvoker supported argument types."); } private static abstract class TypedMethodInvoker { private final Class mArgType; TypedMethodInvoker(Class argType) { mArgType = argType; } boolean invoke(Object receiver, String methodName, String argument) { try { Method method = receiver.getClass().getMethod(methodName, mArgType); method.invoke(receiver, convertArgument(argument)); return true; } catch (NoSuchMethodException ignored) { // ignore } catch (InvocationTargetException e) { LogUtil.w("InvocationTargetException: " + e.getMessage()); } catch (IllegalAccessException e) { LogUtil.w("IllegalAccessException: " + e.getMessage()); } catch (IllegalArgumentException e) { LogUtil.w("IllegalArgumentException: " + e.getMessage()); } return false; } abstract T convertArgument(String argument); } private static class StringMethodInvoker extends TypedMethodInvoker { StringMethodInvoker() { super(String.class); } @Override String convertArgument(String argument) { return argument; } } private static class CharSequenceMethodInvoker extends TypedMethodInvoker { CharSequenceMethodInvoker() { super(CharSequence.class); } @Override CharSequence convertArgument(String argument) { return argument; } } private static class IntegerMethodInvoker extends TypedMethodInvoker { IntegerMethodInvoker() { super(int.class); } @Override Integer convertArgument(String argument) { return Integer.parseInt(argument); } } private static class FloatMethodInvoker extends TypedMethodInvoker { FloatMethodInvoker() { super(float.class); } @Override Float convertArgument(String argument) { return Float.parseFloat(argument); } } private static class BooleanMethodInvoker extends TypedMethodInvoker { BooleanMethodInvoker() { super(boolean.class); } @Override Boolean convertArgument(String argument) { return Boolean.parseBoolean(argument); } } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/elements/android/TextViewDescriptor.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.elements.android; import android.text.Editable; import android.text.TextWatcher; import android.widget.TextView; import com.facebook.stetho.common.Util; import com.facebook.stetho.inspector.elements.AttributeAccumulator; import com.facebook.stetho.inspector.elements.AbstractChainedDescriptor; import java.util.Collections; import java.util.IdentityHashMap; import java.util.Map; final class TextViewDescriptor extends AbstractChainedDescriptor { private static final String TEXT_ATTRIBUTE_NAME = "text"; private final Map mElementToContextMap = Collections.synchronizedMap(new IdentityHashMap()); @Override protected void onHook(final TextView element) { ElementContext context = new ElementContext(); context.hook(element); mElementToContextMap.put(element, context); } protected void onUnhook(TextView element) { ElementContext context = mElementToContextMap.remove(element); context.unhook(); } @Override protected void onGetAttributes(TextView element, AttributeAccumulator attributes) { CharSequence text = element.getText(); if (text != null && text.length() != 0) { attributes.store(TEXT_ATTRIBUTE_NAME, text.toString()); } } private final class ElementContext implements TextWatcher { private TextView mElement; public void hook(TextView element) { mElement = Util.throwIfNull(element); mElement.addTextChangedListener(this); } public void unhook() { if (mElement != null) { mElement.removeTextChangedListener(this); mElement = null; } } @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { } @Override public void afterTextChanged(Editable s) { if (s.length() == 0) { getHost().onAttributeRemoved(mElement, TEXT_ATTRIBUTE_NAME); } else { getHost().onAttributeModified(mElement, TEXT_ATTRIBUTE_NAME, s.toString()); } } } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/elements/android/ViewDescriptor.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.elements.android; import android.graphics.Rect; import android.view.View; import android.view.ViewDebug; import com.facebook.stetho.common.ExceptionUtil; import com.facebook.stetho.common.LogUtil; import com.facebook.stetho.common.ReflectionUtil; import com.facebook.stetho.common.StringUtil; import com.facebook.stetho.common.android.ResourcesUtil; import com.facebook.stetho.inspector.elements.AbstractChainedDescriptor; import com.facebook.stetho.inspector.elements.AttributeAccumulator; import com.facebook.stetho.inspector.elements.ComputedStyleAccumulator; import com.facebook.stetho.inspector.elements.StyleAccumulator; import com.facebook.stetho.inspector.elements.StyleRuleNameAccumulator; import com.facebook.stetho.inspector.helper.IntegerFormatter; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.regex.Pattern; import javax.annotation.Nullable; import javax.annotation.concurrent.GuardedBy; final class ViewDescriptor extends AbstractChainedDescriptor implements HighlightableDescriptor { private static final String ID_NAME = "id"; private static final String NONE_VALUE = "(none)"; private static final String NONE_MAPPING = ""; private static final String VIEW_STYLE_RULE_NAME = ""; private static final String ACCESSIBILITY_STYLE_RULE_NAME = "Accessibility Properties"; private final MethodInvoker mMethodInvoker; private static final boolean sHasSupportNodeInfo; static { sHasSupportNodeInfo = ReflectionUtil.tryGetClassForName( "androidx.core.view.accessibility.AccessibilityNodeInfoCompat") != null; } /** * NOTE: Only access this via {@link #getWordBoundaryPattern}. */ @Nullable private Pattern mWordBoundaryPattern; /** * NOTE: Only access this via {@link #getViewProperties}. */ @Nullable @GuardedBy("this") private volatile List mViewProperties; private Pattern getWordBoundaryPattern() { if (mWordBoundaryPattern == null) { mWordBoundaryPattern = Pattern.compile("(?<=\\p{Lower})(?=\\p{Upper})"); } return mWordBoundaryPattern; } private List getViewProperties() { if (mViewProperties == null) { synchronized (this) { if (mViewProperties == null) { List props = new ArrayList<>(); for (final Method method : View.class.getDeclaredMethods()) { ViewDebug.ExportedProperty annotation = method.getAnnotation( ViewDebug.ExportedProperty.class); if (annotation != null) { props.add(new MethodBackedCSSProperty( method, convertViewPropertyNameToCSSName(method.getName()), annotation)); } } for (final Field field : View.class.getDeclaredFields()) { ViewDebug.ExportedProperty annotation = field.getAnnotation( ViewDebug.ExportedProperty.class); if (annotation != null) { props.add(new FieldBackedCSSProperty( field, convertViewPropertyNameToCSSName(field.getName()), annotation)); } } Collections.sort(props, new Comparator() { @Override public int compare(ViewCSSProperty lhs, ViewCSSProperty rhs) { return lhs.getCSSName().compareTo(rhs.getCSSName()); } }); mViewProperties = Collections.unmodifiableList(props); } } } return mViewProperties; } public ViewDescriptor() { this(new MethodInvoker()); } public ViewDescriptor(MethodInvoker methodInvoker) { mMethodInvoker = methodInvoker; } @Override protected String onGetNodeName(View element) { String className = element.getClass().getName(); return StringUtil.removePrefix(className, "android.view.", StringUtil.removePrefix(className, "android.widget.")); } @Override protected void onGetAttributes(View element, AttributeAccumulator attributes) { String id = getIdAttribute(element); if (id != null) { attributes.store(ID_NAME, id); } } @Override protected void onSetAttributesAsText(View element, String text) { Map attributeToValueMap = parseSetAttributesAsTextArg(text); for (Map.Entry entry : attributeToValueMap.entrySet()) { String methodName = "set" + capitalize(entry.getKey()); String propertyValue = entry.getValue(); mMethodInvoker.invoke(element, methodName, propertyValue); } } @Nullable private static String getIdAttribute(View element) { int id = element.getId(); if (id == View.NO_ID) { return null; } return ResourcesUtil.getIdStringQuietly(element, element.getResources(), id); } @Override @Nullable public View getViewAndBoundsForHighlighting(View element, Rect bounds) { return element; } @Nullable @Override public Object getElementToHighlightAtPosition(View element, int x, int y, Rect bounds) { bounds.set(0, 0, element.getWidth(), element.getHeight()); return element; } @Override protected void onGetStyleRuleNames(View element, StyleRuleNameAccumulator accumulator) { accumulator.store(VIEW_STYLE_RULE_NAME, false); if (sHasSupportNodeInfo) { accumulator.store(ACCESSIBILITY_STYLE_RULE_NAME, false); } } @Override protected void onGetStyles(View element, String ruleName, StyleAccumulator accumulator) { if (VIEW_STYLE_RULE_NAME.equals(ruleName)) { List properties = getViewProperties(); for (int i = 0, size = properties.size(); i < size; i++) { ViewCSSProperty property = properties.get(i); try { getStyleFromValue( element, property.getCSSName(), property.getValue(element), property.getAnnotation(), accumulator); } catch (Exception e) { if (e instanceof IllegalAccessException || e instanceof InvocationTargetException) { LogUtil.e(e, "failed to get style property " + property.getCSSName() + " of element= " + element.toString()); } else { throw ExceptionUtil.propagate(e); } } } } else if (ACCESSIBILITY_STYLE_RULE_NAME.equals(ruleName)) { if (sHasSupportNodeInfo) { boolean ignored = AccessibilityNodeInfoWrapper.getIgnored(element); getStyleFromValue( element, "ignored", ignored, null, accumulator); if (ignored) { getStyleFromValue( element, "ignored-reasons", AccessibilityNodeInfoWrapper.getIgnoredReasons(element), null, accumulator); } getStyleFromValue( element, "focusable", !ignored, null, accumulator); if (!ignored) { getStyleFromValue( element, "focusable-reasons", AccessibilityNodeInfoWrapper.getFocusableReasons(element), null, accumulator); getStyleFromValue( element, "focused", AccessibilityNodeInfoWrapper.getIsAccessibilityFocused(element), null, accumulator); getStyleFromValue( element, "description", AccessibilityNodeInfoWrapper.getDescription(element), null, accumulator); getStyleFromValue( element, "actions", AccessibilityNodeInfoWrapper.getActions(element), null, accumulator); } } } } @Override protected void onGetComputedStyles(View element, ComputedStyleAccumulator styles) { styles.store("left", Integer.toString(element.getLeft())); styles.store("top", Integer.toString(element.getTop())); styles.store("right", Integer.toString(element.getRight())); styles.store("bottom", Integer.toString(element.getBottom())); } private static boolean canIntBeMappedToString(@Nullable ViewDebug.ExportedProperty annotation) { return annotation != null && annotation.mapping() != null && annotation.mapping().length > 0; } private static String mapIntToStringUsingAnnotation( int value, @Nullable ViewDebug.ExportedProperty annotation) { if (!canIntBeMappedToString(annotation)) { throw new IllegalStateException("Cannot map using this annotation"); } for (ViewDebug.IntToString map : annotation.mapping()) { if (map.from() == value) { return map.to(); } } // no mapping was found even though one was expected ): return NONE_MAPPING; } private static boolean canFlagsBeMappedToString(@Nullable ViewDebug.ExportedProperty annotation) { return annotation != null && annotation.flagMapping() != null && annotation.flagMapping().length > 0; } private static String mapFlagsToStringUsingAnnotation( int value, @Nullable ViewDebug.ExportedProperty annotation) { if (!canFlagsBeMappedToString(annotation)) { throw new IllegalStateException("Cannot map using this annotation"); } StringBuilder stringBuilder = null; boolean atLeastOneFlag = false; for (ViewDebug.FlagToString flagToString : annotation.flagMapping()) { if (flagToString.outputIf() == ((value & flagToString.mask()) == flagToString.equals())) { if (stringBuilder == null) { stringBuilder = new StringBuilder(); } if (atLeastOneFlag) { stringBuilder.append(" | "); } stringBuilder.append(flagToString.name()); atLeastOneFlag = true; } } if (atLeastOneFlag) { return stringBuilder.toString(); } else { return NONE_MAPPING; } } private String convertViewPropertyNameToCSSName(String getterName) { // Split string by uppercase characters. Thankfully since // this is the android source we don't have to worry about // internationalization funk. String[] words = getWordBoundaryPattern().split(getterName); StringBuilder result = new StringBuilder(); for (int i = 0; i < words.length; i++) { if (words[i].equals("get") || words[i].equals("m")) { continue; } result.append(words[i].toLowerCase()); if (i < words.length - 1) { result.append('-'); } } return result.toString(); } private void getStyleFromValue( View element, String name, Object value, @Nullable ViewDebug.ExportedProperty annotation, StyleAccumulator styles) { if (name.equals(ID_NAME)) { getIdStyle(element, styles); } else if (value instanceof Integer) { getStyleFromInteger(name, (Integer) value, annotation, styles); } else if (value instanceof Float) { styles.store(name, String.valueOf(value), ((Float) value) == 0.0f); } else if (value instanceof Boolean) { styles.store(name, String.valueOf(value), false); } else if (value instanceof Short) { styles.store(name, String.valueOf(value), ((Short) value) == 0); } else if (value instanceof Long) { styles.store(name, String.valueOf(value), ((Long) value) == 0); } else if (value instanceof Double) { styles.store(name, String.valueOf(value), ((Double) value) == 0.0d); } else if (value instanceof Byte) { styles.store(name, String.valueOf(value), ((Byte) value) == 0); } else if (value instanceof Character) { styles.store(name, String.valueOf(value), ((Character) value) == Character.MIN_VALUE); } else if (value instanceof CharSequence) { styles.store(name, String.valueOf(value), ((CharSequence) value).length() == 0); } else { getStylesFromObject(element, name, value, annotation, styles); } } private void getIdStyle( View element, StyleAccumulator styles) { @Nullable String id = getIdAttribute(element); if (id == null) { styles.store(ID_NAME, NONE_VALUE, false); } else { styles.store(ID_NAME, id, false); } } private void getStyleFromInteger( String name, Integer value, @Nullable ViewDebug.ExportedProperty annotation, StyleAccumulator styles) { String intValueStr = IntegerFormatter.getInstance().format(value, annotation); if (canIntBeMappedToString(annotation)) { styles.store( name, intValueStr + " (" + mapIntToStringUsingAnnotation(value, annotation) + ")", false); } else if (canFlagsBeMappedToString(annotation)) { styles.store( name, intValueStr + " (" + mapFlagsToStringUsingAnnotation(value, annotation) + ")", false); } else { Boolean defaultValue = true; // Mappable ints should always be shown, because enums don't necessarily have // logical "default" values. Thus we mark all of them as not default, so that they // show up in the inspector. if (value != 0 || canFlagsBeMappedToString(annotation) || canIntBeMappedToString(annotation)) { defaultValue = false; } styles.store(name, intValueStr, defaultValue); } } private void getStylesFromObject( View view, String name, Object value, @Nullable ViewDebug.ExportedProperty annotation, StyleAccumulator styles) { if (annotation == null || !annotation.deepExport() || value == null) { return; } Field[] fields = value.getClass().getFields(); for (Field field : fields) { int modifiers = field.getModifiers(); if (Modifier.isStatic(modifiers)) { continue; } Object propertyValue; try { field.setAccessible(true); propertyValue = field.get(value); } catch (IllegalAccessException e) { LogUtil.e( e, "failed to get property of name: \"" + name + "\" of object: " + String.valueOf(value)); return; } String propertyName = field.getName(); switch (propertyName) { case "bottomMargin": propertyName = "margin-bottom"; break; case "topMargin": propertyName = "margin-top"; break; case "leftMargin": propertyName = "margin-left"; break; case "rightMargin": propertyName = "margin-right"; break; default: String annotationPrefix = annotation.prefix(); propertyName = convertViewPropertyNameToCSSName( (annotationPrefix == null) ? propertyName : (annotationPrefix + propertyName)); break; } ViewDebug.ExportedProperty subAnnotation = field.getAnnotation(ViewDebug.ExportedProperty.class); getStyleFromValue( view, propertyName, propertyValue, subAnnotation, styles); } } private static String capitalize(String str) { if (str == null || str.length() == 0 || Character.isTitleCase(str.charAt(0))) { return str; } StringBuilder buffer = new StringBuilder(str); buffer.setCharAt(0, Character.toTitleCase(buffer.charAt(0))); return buffer.toString(); } private final class FieldBackedCSSProperty extends ViewCSSProperty { private final Field mField; public FieldBackedCSSProperty( Field field, String cssName, @Nullable ViewDebug.ExportedProperty annotation) { super(cssName, annotation); mField = field; mField.setAccessible(true); } @Override public Object getValue(View view) throws InvocationTargetException, IllegalAccessException { return mField.get(view); } } private final class MethodBackedCSSProperty extends ViewCSSProperty { private final Method mMethod; public MethodBackedCSSProperty( Method method, String cssName, @Nullable ViewDebug.ExportedProperty annotation) { super(cssName, annotation); mMethod = method; mMethod.setAccessible(true); } @Override public Object getValue(View view) throws InvocationTargetException, IllegalAccessException { return mMethod.invoke(view); } } private abstract class ViewCSSProperty { private final String mCSSName; private final ViewDebug.ExportedProperty mAnnotation; public ViewCSSProperty(String cssName, @Nullable ViewDebug.ExportedProperty annotation) { mCSSName = cssName; mAnnotation = annotation; } public final String getCSSName() { return mCSSName; } public abstract Object getValue(View view) throws InvocationTargetException, IllegalAccessException; public final @Nullable ViewDebug.ExportedProperty getAnnotation() { return mAnnotation; } } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/elements/android/ViewGroupDescriptor.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.elements.android; import android.graphics.Rect; import android.view.View; import android.view.ViewGroup; import com.facebook.stetho.common.Accumulator; import com.facebook.stetho.common.android.FragmentCompatUtil; import com.facebook.stetho.inspector.elements.AbstractChainedDescriptor; import java.lang.ref.WeakReference; import java.util.Collections; import java.util.Map; import java.util.WeakHashMap; import javax.annotation.Nullable; final class ViewGroupDescriptor extends AbstractChainedDescriptor implements HighlightableDescriptor { /** * This is a cache that maps from a View to the Fragment that contains it. If the View isn't * contained by a Fragment, then this maps the View to itself. For Views contained by Fragments, * we emit the Fragment instead, and then let the Fragment's descriptor emit the View as its sole * child. This allows us to see Fragments in the inspector as part of the UI tree. */ private final Map mViewToElementMap = Collections.synchronizedMap(new WeakHashMap()); public ViewGroupDescriptor() { } @Override protected void onGetChildren(ViewGroup element, Accumulator children) { for (int i = 0, N = element.getChildCount(); i < N; ++i) { final View childView = element.getChildAt(i); if (isChildVisible(childView)) { final Object childElement = getElementForView(element, childView); children.store(childElement); } } } private boolean isChildVisible(View child) { return !(child instanceof DocumentHiddenView); } private Object getElementForView(ViewGroup parentView, View childView) { Object value = mViewToElementMap.get(childView); if (value != null) { Object element = getElement(childView, value); // The parent of a View may have changed since we stashed it into the cache. // If that's the case then we can't use the cache's answer. if (element != null && childView.getParent() == parentView) { return element; } mViewToElementMap.remove(childView); } /** * Note that we do NOT emit DialogFragments. Those get emitted via ActivityDescriptor. * We do the check here so that we can also cache the cost of calling * {@link FragmentCompatUtil#isDialogFragment(Object)}. */ Object fragment = FragmentCompatUtil.findFragmentForView(childView); if (fragment != null && !FragmentCompatUtil.isDialogFragment(fragment)) { mViewToElementMap.put(childView, new WeakReference<>(fragment)); return fragment; } else { // No need to store a strong reference to the childView in the value. We'll just store this // object and when pull the value out of the map we'll check for this object and just use the // key instead. mViewToElementMap.put(childView, this); return childView; } } @SuppressWarnings("unchecked") private Object getElement(View childView, Object value) { if (value == this) { return childView; } else { return ((WeakReference) value).get(); } } @Override @Nullable public View getViewAndBoundsForHighlighting(ViewGroup element, Rect bounds) { return element; } @Nullable @Override public Object getElementToHighlightAtPosition(ViewGroup element, int x, int y, Rect bounds) { View hitChild = null; for (int i = element.getChildCount() - 1; i >= 0; --i) { final View childView = element.getChildAt(i); if (isChildVisible(childView) && childView.getVisibility() == View.VISIBLE) { childView.getHitRect(bounds); if (bounds.contains(x, y)) { hitChild = childView; break; } } } if (hitChild != null) { return hitChild; } else { bounds.set(0, 0, element.getWidth(), element.getHeight()); return element; } } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/elements/android/ViewHighlightOverlays.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.elements.android; import android.annotation.TargetApi; import android.graphics.Canvas; import android.graphics.Rect; import android.graphics.Region; import android.graphics.drawable.ColorDrawable; import android.os.Build; import android.view.View; import android.view.ViewGroup; import android.view.ViewGroup.MarginLayoutParams; abstract class ViewHighlightOverlays { abstract void highlightView(View view, Rect bounds, int mainColor); abstract void removeHighlight(View view); static ViewHighlightOverlays newInstance() { // This may not be needed since ViewHighlighter.newInstance() is already instantiating a // NoopHighlighter for SDK_INT < JELLY_BEAN_MR2, but just to make sure... if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { return new ViewHighlightOverlaysJellybeanMR2(); } return new NoOpViewHighlightOverlays(); } private static class NoOpViewHighlightOverlays extends ViewHighlightOverlays { @Override void highlightView(View view, Rect bounds, int mainColor) { } @Override void removeHighlight(View view) { } } @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) private static class ViewHighlightOverlaysJellybeanMR2 extends ViewHighlightOverlays { private static final int MARGIN_OVERLAY_COLOR = 0xaaf4ca9e; private static final int PADDING_OVERLAY_COLOR = 0xaabedab6; private final MainHighlightDrawable mMainHighlightDrawable = new MainHighlightDrawable(); private final HighlightDrawable[] mHighlightDrawables = { mMainHighlightDrawable, new PaddingTopHighlightDrawable(), new PaddingBottomHighlightDrawable(), new PaddingRightHighlightDrawable(), new PaddingLeftHighlightDrawable(), new MarginTopHighlightDrawable(), new MarginBottomHighlightDrawable(), new MarginRightHighlightDrawable(), new MarginLeftHighlightDrawable() }; ViewHighlightOverlaysJellybeanMR2() { } @Override void highlightView(View view, Rect bounds, int mainColor) { mMainHighlightDrawable.setColor(mainColor); if (bounds.isEmpty()) { mMainHighlightDrawable.setBounds(0, 0, view.getWidth(), view.getHeight()); } else { mMainHighlightDrawable.setBounds(bounds); } int total = mHighlightDrawables.length; for (int i = 0; i < total; i++) { HighlightDrawable drawable = mHighlightDrawables[i]; drawable.highlightView(view); view.getOverlay().add(drawable); } } @Override void removeHighlight(View view) { for (ColorDrawable drawable : mHighlightDrawables) { view.getOverlay().remove(drawable); } } static abstract class HighlightDrawable extends ColorDrawable { protected final Rect mMargins = new Rect(); protected final Rect mPaddings = new Rect(); HighlightDrawable(int color) { super(color); } public HighlightDrawable() { } void highlightView(View view) { ViewGroup.LayoutParams layoutParams = view.getLayoutParams(); if (layoutParams instanceof MarginLayoutParams) { MarginLayoutParams marginLayoutParams = (MarginLayoutParams) layoutParams; mMargins.left = marginLayoutParams.leftMargin; mMargins.top = marginLayoutParams.topMargin; mMargins.right = marginLayoutParams.rightMargin; mMargins.bottom = marginLayoutParams.bottomMargin; } else { mMargins.left = 0; mMargins.top = 0; mMargins.right = 0; mMargins.bottom = 0; } mPaddings.left = view.getPaddingLeft(); mPaddings.top = view.getPaddingTop(); mPaddings.right = view.getPaddingRight(); mPaddings.bottom = view.getPaddingBottom(); } } static class MainHighlightDrawable extends HighlightDrawable { @Override void highlightView(View view) { super.highlightView(view); } @Override public void draw(Canvas canvas) { // We don't have access to the OverlayViewGroup instance directly, but we can manipulate // its Canvas via the Drawables' draw(). This allows us to draw outside the View bounds, // so we can position the margin overlays correctly. Rect newRect = canvas.getClipBounds(); // Make the Canvas Rect bigger according to the View margins. newRect.inset(-(mMargins.right + mMargins.left), -(mMargins.top + mMargins.bottom)); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { canvas.clipRect(newRect, Region.Op.REPLACE); } else { canvas.clipOutRect(newRect); } super.draw(canvas); } } static class PaddingTopHighlightDrawable extends HighlightDrawable { PaddingTopHighlightDrawable() { super(PADDING_OVERLAY_COLOR); } @Override void highlightView(View view) { super.highlightView(view); setBounds(mPaddings.left, 0, view.getWidth() - mPaddings.right, mPaddings.top); } } static class PaddingBottomHighlightDrawable extends HighlightDrawable { PaddingBottomHighlightDrawable() { super(PADDING_OVERLAY_COLOR); } @Override void highlightView(View view) { super.highlightView(view); setBounds(mPaddings.left, view.getHeight() - mPaddings.bottom, view.getWidth() - mPaddings.right, view.getHeight()); } } static class PaddingRightHighlightDrawable extends HighlightDrawable { PaddingRightHighlightDrawable() { super(PADDING_OVERLAY_COLOR); } @Override void highlightView(View view) { super.highlightView(view); setBounds(view.getWidth() - mPaddings.right, 0, view.getWidth(), view.getHeight()); } } static class PaddingLeftHighlightDrawable extends HighlightDrawable { PaddingLeftHighlightDrawable() { super(PADDING_OVERLAY_COLOR); } @Override void highlightView(View view) { super.highlightView(view); setBounds(0, 0, mPaddings.left, view.getHeight()); } } static class MarginTopHighlightDrawable extends HighlightDrawable { MarginTopHighlightDrawable() { super(MARGIN_OVERLAY_COLOR); } @Override void highlightView(View view) { super.highlightView(view); setBounds(0, 0, view.getWidth(), mMargins.top); } @Override public void draw(Canvas canvas) { canvas.translate(0, -mMargins.top); super.draw(canvas); } } static class MarginBottomHighlightDrawable extends HighlightDrawable { MarginBottomHighlightDrawable() { super(MARGIN_OVERLAY_COLOR); } @Override void highlightView(View view) { super.highlightView(view); setBounds(0, view.getHeight() - mMargins.bottom, view.getWidth(), view.getHeight()); } @Override public void draw(Canvas canvas) { canvas.translate(0, mMargins.bottom + mMargins.top); super.draw(canvas); } } static class MarginRightHighlightDrawable extends HighlightDrawable { MarginRightHighlightDrawable() { super(MARGIN_OVERLAY_COLOR); } @Override void highlightView(View view) { super.highlightView(view); setBounds(view.getWidth() - mMargins.right, 0, view.getWidth(), view.getHeight() + mMargins.top + mMargins.bottom); } @Override public void draw(Canvas canvas) { canvas.translate(mMargins.right, -(mMargins.top + mMargins.bottom)); super.draw(canvas); } } static class MarginLeftHighlightDrawable extends HighlightDrawable { MarginLeftHighlightDrawable() { super(MARGIN_OVERLAY_COLOR); } @Override void highlightView(View view) { super.highlightView(view); setBounds(0, 0, mMargins.left, view.getHeight() + mMargins.top + mMargins.bottom); } @Override public void draw(Canvas canvas) { canvas.translate(-(mMargins.left + mMargins.right), 0); super.draw(canvas); } } } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/elements/android/ViewHighlighter.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.elements.android; import android.annotation.TargetApi; import android.graphics.Rect; import android.os.Build; import android.os.Handler; import android.os.Looper; import android.view.View; import com.facebook.stetho.common.LogUtil; import com.facebook.stetho.common.Util; import javax.annotation.Nullable; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; abstract class ViewHighlighter { public static ViewHighlighter newInstance() { // TODO: find ways to do highlighting on older versions too if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { return new OverlayHighlighter(); } else { LogUtil.w("Running on pre-JBMR2: View highlighting is not supported"); return new NoopHighlighter(); } } protected ViewHighlighter() { } public abstract void clearHighlight(); public abstract void setHighlightedView(View view, @Nullable Rect bounds, int color); private static final class NoopHighlighter extends ViewHighlighter { @Override public void clearHighlight() { } @Override public void setHighlightedView(View view, @Nullable Rect bounds, int color) { } } @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) private static final class OverlayHighlighter extends ViewHighlighter { // TODO: use the top-level ViewGroupOverlay instead of ViewOverlay so that we don't end up // causing every single view to allocate a ViewOverlay private final Handler mHandler; private final ViewHighlightOverlays mHighlightOverlays = ViewHighlightOverlays.newInstance(); // Only assigned on the UI thread private View mHighlightedView; private final Rect mHighlightedBounds = new Rect(); private final Rect mEmptyRect = new Rect(); private AtomicReference mViewToHighlight = new AtomicReference(); private AtomicReference mBoundsToHighlight = new AtomicReference(); private AtomicInteger mContentColor = new AtomicInteger(); private final Runnable mHighlightViewOnUiThreadRunnable = new Runnable() { @Override public void run() { highlightViewOnUiThread(); } }; public OverlayHighlighter() { mHandler = new Handler(Looper.getMainLooper()); } @Override public void clearHighlight() { setHighlightedViewImpl(null, null, 0); } @Override public void setHighlightedView(View view, @Nullable Rect bounds, int color) { setHighlightedViewImpl(Util.throwIfNull(view), bounds, color); } private void setHighlightedViewImpl(@Nullable View view, @Nullable Rect bounds, int color) { mHandler.removeCallbacks(mHighlightViewOnUiThreadRunnable); mViewToHighlight.set(view); mBoundsToHighlight.set(bounds); mContentColor.set(color); mHandler.postDelayed(mHighlightViewOnUiThreadRunnable, 100); } private void highlightViewOnUiThread() { final View viewToHighlight = mViewToHighlight.getAndSet(null); Rect boundsToHighlight = mBoundsToHighlight.getAndSet(null); if (boundsToHighlight == null) { boundsToHighlight = mEmptyRect; } if (viewToHighlight == mHighlightedView && mHighlightedBounds.equals(boundsToHighlight)) { return; } if (mHighlightedView != null) { mHighlightOverlays.removeHighlight(mHighlightedView); } if (viewToHighlight != null) { mHighlightOverlays.highlightView( viewToHighlight, boundsToHighlight, mContentColor.get()); } mHighlightedView = viewToHighlight; if (boundsToHighlight == null) { mHighlightedBounds.setEmpty(); } else { mHighlightedBounds.set(boundsToHighlight); } } } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/elements/android/WindowDescriptor.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.elements.android; import android.graphics.Rect; import android.view.View; import android.view.Window; import com.facebook.stetho.common.Accumulator; import com.facebook.stetho.inspector.elements.AbstractChainedDescriptor; import com.facebook.stetho.inspector.elements.Descriptor; import javax.annotation.Nullable; final class WindowDescriptor extends AbstractChainedDescriptor implements HighlightableDescriptor { @Override protected void onGetChildren(Window element, Accumulator children) { View decorView = element.peekDecorView(); if (decorView != null) { children.store(decorView); } } @Override @Nullable public View getViewAndBoundsForHighlighting(Window element, Rect bounds) { return element.peekDecorView(); } @Nullable @Override public Object getElementToHighlightAtPosition(Window element, int x, int y, Rect bounds) { final Descriptor.Host host = getHost(); View view = null; HighlightableDescriptor descriptor = null; if (host instanceof AndroidDescriptorHost) { view = element.peekDecorView(); descriptor = ((AndroidDescriptorHost) host).getHighlightableDescriptor(view); } return descriptor == null ? null : descriptor.getElementToHighlightAtPosition(view, x, y, bounds); } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/elements/android/window/WindowRootViewCompactV16Impl.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.elements.android.window; import android.content.Context; import android.view.View; import android.view.WindowManager; import java.lang.reflect.Field; import java.util.Arrays; import java.util.Collections; import java.util.List; import androidx.annotation.NonNull; class WindowRootViewCompactV16Impl extends WindowRootViewCompat { private Context mContext; WindowRootViewCompactV16Impl(Context context) { this.mContext = context; } @NonNull @Override public List getRootViews() { WindowManager windowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); Object wm = getOuter(windowManager); return getWindowViews(wm); } private static Object getOuter(Object innerWM) { try { Field parentField = innerWM.getClass().getDeclaredField("mWindowManager"); parentField.setAccessible(true); Object outerWM = parentField.get(innerWM); parentField.setAccessible(false); return outerWM; } catch (NoSuchFieldException e) { throw new RuntimeException(e); } catch (IllegalAccessException e) { throw new RuntimeException(e); } } private static List getWindowViews(final Object windowManager) { try { Class clz = windowManager.getClass(); Field field = clz.getDeclaredField("mViews"); field.setAccessible(true); return Collections.unmodifiableList(Arrays.asList((View[]) field.get(windowManager))); } catch (NoSuchFieldException e) { throw new RuntimeException(e); } catch (IllegalAccessException e) { throw new RuntimeException(e); } } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/elements/android/window/WindowRootViewCompactV18Impl.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.elements.android.window; import android.view.View; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Arrays; import java.util.Collections; import java.util.List; import androidx.annotation.NonNull; class WindowRootViewCompactV18Impl extends WindowRootViewCompat { private Field mViewsField; private Object mWindowManagerGlobal; WindowRootViewCompactV18Impl() { try { Class wmClz = Class.forName("android.view.WindowManagerGlobal"); Method getInstanceMethod = wmClz.getDeclaredMethod("getInstance"); mWindowManagerGlobal = getInstanceMethod.invoke(wmClz); mViewsField = wmClz.getDeclaredField("mViews"); mViewsField.setAccessible(true); } catch (ClassNotFoundException e) { throw new RuntimeException(e); } catch (NoSuchMethodException e) { throw new RuntimeException(e); } catch (IllegalAccessException e) { throw new RuntimeException(e); } catch (InvocationTargetException e) { throw new RuntimeException(e); } catch (NoSuchFieldException e) { throw new RuntimeException(e); } } @NonNull @Override public List getRootViews() { try { return Collections.unmodifiableList(Arrays.asList((View[]) mViewsField.get(mWindowManagerGlobal))); } catch (IllegalAccessException e) { throw new RuntimeException(e); } } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/elements/android/window/WindowRootViewCompactV19Impl.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.elements.android.window; import android.view.View; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Collections; import java.util.List; import androidx.annotation.NonNull; class WindowRootViewCompactV19Impl extends WindowRootViewCompat { private List mRootViews; WindowRootViewCompactV19Impl() { try { Class wmClz = Class.forName("android.view.WindowManagerGlobal"); Method getInstanceMethod = wmClz.getDeclaredMethod("getInstance"); Object managerGlobal = getInstanceMethod.invoke(wmClz); Field mViewsField = wmClz.getDeclaredField("mViews"); mViewsField.setAccessible(true); mRootViews = (List) mViewsField.get(managerGlobal); mViewsField.setAccessible(false); } catch (ClassNotFoundException e) { throw new RuntimeException(e); } catch (NoSuchMethodException e) { throw new RuntimeException(e); } catch (IllegalAccessException e) { throw new RuntimeException(e); } catch (InvocationTargetException e) { throw new RuntimeException(e); } catch (NoSuchFieldException e) { throw new RuntimeException(e); } } @NonNull @Override public List getRootViews() { return Collections.unmodifiableList(mRootViews); } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/elements/android/window/WindowRootViewCompat.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.elements.android.window; import android.content.Context; import android.os.Build; import android.view.View; import com.facebook.stetho.common.Util; import java.util.List; import androidx.annotation.NonNull; /** * get the root view of all windows *

* when you add view by windowManager.addView(), the root view may not be a DecorView *

*

* There are some differences when you want to get the root view *

* 4.0.3_r1 WindowManagerImpl private View[] mViews; *

* 4.0.4 WindowManagerImpl private View[] mViews; *

* 4.1.1 WindowManagerImpl private View[] mViews; *

* 4.1.2 WindowManagerImpl private View[] mViews; *

* 4.2_r1 WindowManagerGlobal private View[] mViews *

* 4.2.2 r1 WindowManagerGlobal private View[] mViews *

* 4.3_r2.1 WindowManagerGlobal private View[] mViews; *

* 4.4_r1 WindowManagerGlobal private final ArrayList<View> mViews *

* 4.4.2_r1 WindowManagerGlobal private final ArrayList<View> mViews *

* 5.0.0_r2 WindowManagerGlobal private final ArrayList<View> mViews *

* 6.0.0_r1 WindowManagerGlobal private final ArrayList<View> mViews *

* 7.0.0_r1 WindowManagerGlobal private final ArrayList<View> mViews *

* 8.0.0_r4 WindowManagerGlobal private final ArrayList<View> mViews */ public abstract class WindowRootViewCompat { private static WindowRootViewCompat sInstance; public static WindowRootViewCompat get(Context context) { if (sInstance != null) { return sInstance; } Util.throwIfNull(context); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { sInstance = new WindowRootViewCompactV19Impl(); } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN_MR1 || Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN_MR2) { sInstance = new WindowRootViewCompactV18Impl(); } else { sInstance = new WindowRootViewCompactV16Impl(context.getApplicationContext()); } return sInstance; } @NonNull public abstract List getRootViews(); } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/helper/ChromePeerManager.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.helper; import javax.annotation.Nullable; import javax.annotation.concurrent.GuardedBy; import java.nio.channels.NotYetConnectedException; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import com.facebook.stetho.common.LogRedirector; import com.facebook.stetho.common.Util; import com.facebook.stetho.inspector.jsonrpc.DisconnectReceiver; import com.facebook.stetho.inspector.jsonrpc.JsonRpcPeer; import com.facebook.stetho.inspector.jsonrpc.PendingRequestCallback; /** * Interface glue that allows a particular domain to manage the enabled peers. The way the * WebKit inspector protocol works is that each functionality domain has an enable/disable JSON-RPC * method call which alerts the server (that's us) that we can now begin sending local events * to the peer to have them appear in the inspector UI. This class simplifies managing those * enabled peers for each functionality domain. */ public class ChromePeerManager { private static final String TAG = "ChromePeerManager"; /** * Set of registered peers, mapped to the disconnect receiver for automatic unregistration * purposes. */ @GuardedBy("this") private final Map mReceivingPeers = new HashMap<>(); /** * This should be set to null anytime mReceivingPeers is changed. It should always be * retrieved by calling getReceivingPeersSnapshot(). */ @GuardedBy("this") private JsonRpcPeer[] mReceivingPeersSnapshot; @GuardedBy("this") private PeerRegistrationListener mListener; public ChromePeerManager() { } /** * Set a listener which can receive notifications of unique registration event (see * {@link #addPeer} and {@link #removePeer}). * * @param listener */ public synchronized void setListener(PeerRegistrationListener listener) { mListener = listener; } /** * Register a new peer, adding them to an internal list of receivers. * * @param peer * @return True if this is a newly registered peer; false if it was already registered. */ public synchronized boolean addPeer(JsonRpcPeer peer) { if (mReceivingPeers.containsKey(peer)) { return false; } DisconnectReceiver disconnectReceiver = new UnregisterOnDisconnect(peer); peer.registerDisconnectReceiver(disconnectReceiver); mReceivingPeers.put(peer, disconnectReceiver); mReceivingPeersSnapshot = null; if (mListener != null) { mListener.onPeerRegistered(peer); } return true; } /** * Unregister an existing peer. * * @param peer */ public synchronized void removePeer(JsonRpcPeer peer) { if (mReceivingPeers.remove(peer) != null) { mReceivingPeersSnapshot = null; if (mListener != null) { mListener.onPeerUnregistered(peer); } } } public synchronized boolean hasRegisteredPeers() { return !mReceivingPeers.isEmpty(); } private synchronized JsonRpcPeer[] getReceivingPeersSnapshot() { if (mReceivingPeersSnapshot == null) { mReceivingPeersSnapshot = mReceivingPeers.keySet().toArray( new JsonRpcPeer[mReceivingPeers.size()]); } return mReceivingPeersSnapshot; } public void sendNotificationToPeers(String method, Object params) { sendMessageToPeers(method, params, null /* callback */); } public void invokeMethodOnPeers(String method, Object params, PendingRequestCallback callback) { Util.throwIfNull(callback); sendMessageToPeers(method, params, callback); } private void sendMessageToPeers(String method, Object params, @Nullable PendingRequestCallback callback) { JsonRpcPeer[] peers = getReceivingPeersSnapshot(); for (JsonRpcPeer peer : peers) { try { peer.invokeMethod(method, params, callback); } catch (NotYetConnectedException e) { LogRedirector.e(TAG, "Error delivering data to Chrome", e); } } } private class UnregisterOnDisconnect implements DisconnectReceiver { private final JsonRpcPeer mPeer; public UnregisterOnDisconnect(JsonRpcPeer peer) { mPeer = peer; } @Override public void onDisconnect() { removePeer(mPeer); } } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/helper/IntegerFormatter.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.helper; import android.annotation.TargetApi; import android.os.Build; import android.view.ViewDebug; import androidx.annotation.Nullable; public class IntegerFormatter { private static IntegerFormatter cachedFormatter; public static IntegerFormatter getInstance() { if (cachedFormatter == null) { synchronized (IntegerFormatter.class) { if (cachedFormatter == null) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { cachedFormatter = new IntegerFormatterWithHex(); } else { cachedFormatter = new IntegerFormatter(); } } } } return cachedFormatter; } private IntegerFormatter() { } public String format(Integer integer, @Nullable ViewDebug.ExportedProperty annotation) { return String.valueOf(integer); } private static class IntegerFormatterWithHex extends IntegerFormatter { @Override @TargetApi(Build.VERSION_CODES.LOLLIPOP) public String format(Integer integer, @Nullable ViewDebug.ExportedProperty annotation) { if (annotation != null && annotation.formatToHexString()) { return "0x" + Integer.toHexString(integer); } return super.format(integer, annotation); } } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/helper/ObjectIdMapper.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.helper; import android.util.SparseArray; import java.util.IdentityHashMap; import java.util.Map; import javax.annotation.Nullable; import javax.annotation.concurrent.GuardedBy; public class ObjectIdMapper { protected final Object mSync = new Object(); @GuardedBy("mSync") private int mNextId = 1; @GuardedBy("mSync") private final Map mObjectToIdMap = new IdentityHashMap(); @GuardedBy("mSync") private SparseArray mIdToObjectMap = new SparseArray(); public void clear() { SparseArray idToObjectMap; synchronized (mSync) { idToObjectMap = mIdToObjectMap; mObjectToIdMap.clear(); mIdToObjectMap = new SparseArray(); } int size = idToObjectMap.size(); for (int i = 0; i < size; ++i) { int id = idToObjectMap.keyAt(i); Object object = idToObjectMap.valueAt(i); onUnmapped(object, id); } } public boolean containsId(int id) { synchronized (mSync) { return mIdToObjectMap.get(id) != null; } } public boolean containsObject(Object object) { synchronized (mSync) { return mObjectToIdMap.containsKey(object); } } @Nullable public Object getObjectForId(int id) { synchronized (mSync) { return mIdToObjectMap.get(id); } } @Nullable public Integer getIdForObject(Object object) { synchronized (mSync) { return mObjectToIdMap.get(object); } } public int putObject(Object object) { Integer id; synchronized (mSync) { id = mObjectToIdMap.get(object); if (id != null) { return id; } id = mNextId++; mObjectToIdMap.put(object, id); mIdToObjectMap.put(id, object); } onMapped(object, id); return id; } @Nullable public Object removeObjectById(int id) { Object object; synchronized (mSync) { object = mIdToObjectMap.get(id); if (object == null) { return null; } mIdToObjectMap.remove(id); mObjectToIdMap.remove(object); } onUnmapped(object, id); return object; } @Nullable public Integer removeObject(Object object) { Integer id; synchronized (mSync) { id = mObjectToIdMap.remove(object); if (id == null) { return null; } mIdToObjectMap.remove(id); } onUnmapped(object, id); return id; } public int size() { synchronized (mSync) { return mObjectToIdMap.size(); } } protected void onMapped(Object object, int id) { } protected void onUnmapped(Object object, int id) { } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/helper/PeerRegistrationListener.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.helper; import com.facebook.stetho.inspector.jsonrpc.JsonRpcPeer; public interface PeerRegistrationListener { void onPeerRegistered(JsonRpcPeer peer); void onPeerUnregistered(JsonRpcPeer peer); } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/helper/PeersRegisteredListener.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.helper; import java.util.concurrent.atomic.AtomicInteger; import com.facebook.stetho.inspector.jsonrpc.JsonRpcPeer; public abstract class PeersRegisteredListener implements PeerRegistrationListener { private AtomicInteger mPeers = new AtomicInteger(0); @Override public final void onPeerRegistered(JsonRpcPeer peer) { if (mPeers.incrementAndGet() == 1) { onFirstPeerRegistered(); } onPeerAdded(peer); } @Override public final void onPeerUnregistered(JsonRpcPeer peer) { if (mPeers.decrementAndGet() == 0) { onLastPeerUnregistered(); } onPeerRemoved(peer); } protected void onPeerAdded(JsonRpcPeer peer) {} protected void onPeerRemoved(JsonRpcPeer peer) {} protected abstract void onFirstPeerRegistered(); protected abstract void onLastPeerUnregistered(); } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/helper/ThreadBoundProxy.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.helper; import android.os.Handler; import com.facebook.stetho.common.ThreadBound; import com.facebook.stetho.common.UncheckedCallable; import com.facebook.stetho.common.Util; import com.facebook.stetho.common.android.HandlerUtil; /** * This class is for those cases when a class' threading * policy is determined by one of its member variables. */ public abstract class ThreadBoundProxy implements ThreadBound { private final ThreadBound mEnforcer; public ThreadBoundProxy(ThreadBound enforcer) { mEnforcer = Util.throwIfNull(enforcer); } @Override public final boolean checkThreadAccess() { return mEnforcer.checkThreadAccess(); } @Override public final void verifyThreadAccess() { mEnforcer.verifyThreadAccess(); } @Override public final V postAndWait(UncheckedCallable c) { return mEnforcer.postAndWait(c); } @Override public final void postAndWait(Runnable r) { mEnforcer.postAndWait(r); } @Override public final void postDelayed(Runnable r, long delayMillis) { mEnforcer.postDelayed(r, delayMillis); } @Override public final void removeCallbacks(Runnable r) { mEnforcer.removeCallbacks(r); } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/jsonrpc/DisconnectReceiver.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.jsonrpc; /** * @see JsonRpcPeer#registerDisconnectReceiver(DisconnectReceiver) */ public interface DisconnectReceiver { /** * Invoked when a WebSocket peer disconnects. */ void onDisconnect(); } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/jsonrpc/JsonRpcException.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.jsonrpc; import com.facebook.stetho.inspector.jsonrpc.protocol.JsonRpcError; import com.facebook.stetho.common.Util; public class JsonRpcException extends Exception { private final JsonRpcError mErrorMessage; public JsonRpcException(JsonRpcError errorMessage) { super(errorMessage.code + ": " + errorMessage.message); mErrorMessage = Util.throwIfNull(errorMessage); } public JsonRpcError getErrorMessage() { return mErrorMessage; } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/jsonrpc/JsonRpcPeer.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.jsonrpc; import javax.annotation.Nullable; import javax.annotation.concurrent.GuardedBy; import javax.annotation.concurrent.ThreadSafe; import java.nio.channels.NotYetConnectedException; import java.util.HashMap; import java.util.Map; import android.database.Observable; import com.facebook.stetho.inspector.jsonrpc.protocol.JsonRpcRequest; import com.facebook.stetho.common.Util; import com.facebook.stetho.json.ObjectMapper; import com.facebook.stetho.websocket.SimpleSession; import org.json.JSONObject; @ThreadSafe public class JsonRpcPeer { private final SimpleSession mPeer; private final ObjectMapper mObjectMapper; @GuardedBy("this") private long mNextRequestId; @GuardedBy("this") private final Map mPendingRequests = new HashMap<>(); private final DisconnectObservable mDisconnectObservable = new DisconnectObservable(); public JsonRpcPeer(ObjectMapper objectMapper, SimpleSession peer) { mObjectMapper = objectMapper; mPeer = Util.throwIfNull(peer); } public SimpleSession getWebSocket() { return mPeer; } public void invokeMethod(String method, Object paramsObject, @Nullable PendingRequestCallback callback) throws NotYetConnectedException { Util.throwIfNull(method); Long requestId = (callback != null) ? preparePendingRequest(callback) : null; // magic, can basically convert anything for some amount of runtime overhead... JSONObject params = mObjectMapper.convertValue(paramsObject, JSONObject.class); JsonRpcRequest message = new JsonRpcRequest(requestId, method, params); String requestString; JSONObject jsonObject = mObjectMapper.convertValue(message, JSONObject.class); requestString = jsonObject.toString(); mPeer.sendText(requestString); } public void registerDisconnectReceiver(DisconnectReceiver callback) { mDisconnectObservable.registerObserver(callback); } public void unregisterDisconnectReceiver(DisconnectReceiver callback) { mDisconnectObservable.unregisterObserver(callback); } public void invokeDisconnectReceivers() { mDisconnectObservable.onDisconnect(); } private synchronized long preparePendingRequest(PendingRequestCallback callback) { long requestId = mNextRequestId++; mPendingRequests.put(requestId, new PendingRequest(requestId, callback)); return requestId; } public synchronized PendingRequest getAndRemovePendingRequest(long requestId) { return mPendingRequests.remove(requestId); } private static class DisconnectObservable extends Observable { public void onDisconnect() { for (int i = 0, N = mObservers.size(); i < N; ++i) { final DisconnectReceiver observer = mObservers.get(i); observer.onDisconnect(); } } } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/jsonrpc/JsonRpcResult.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.jsonrpc; import com.facebook.stetho.inspector.jsonrpc.protocol.JsonRpcResponse; /** * Marker interface used to denote a JSON-RPC result. After conversion from Jackson, * this will be placed into {@link JsonRpcResponse#result}. */ public interface JsonRpcResult { } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/jsonrpc/PendingRequest.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.jsonrpc; import javax.annotation.Nullable; /** * Represents an outstanding request to the peer (issued by us). This callback will be * fired when the server responds. Note that with JSON-RPC, there is a special kind of * request called a notification which does not require a callback (and thus won't use * this class). */ public class PendingRequest { public final long requestId; public final @Nullable PendingRequestCallback callback; public PendingRequest(long requestId, @Nullable PendingRequestCallback callback) { this.requestId = requestId; this.callback = callback; } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/jsonrpc/PendingRequestCallback.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.jsonrpc; import com.facebook.stetho.inspector.jsonrpc.protocol.JsonRpcResponse; public interface PendingRequestCallback { void onResponse(JsonRpcPeer peer, JsonRpcResponse response); } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/jsonrpc/protocol/EmptyResult.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.jsonrpc.protocol; import com.facebook.stetho.inspector.jsonrpc.JsonRpcResult; public class EmptyResult implements JsonRpcResult { } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/jsonrpc/protocol/JsonRpcError.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.jsonrpc.protocol; import javax.annotation.Nullable; import android.annotation.SuppressLint; import com.facebook.stetho.json.annotation.JsonProperty; import com.facebook.stetho.json.annotation.JsonValue; import org.json.JSONObject; @SuppressLint({ "UsingDefaultJsonDeserializer", "EmptyJsonPropertyUse" }) public class JsonRpcError { @JsonProperty(required = true) public ErrorCode code; @JsonProperty(required = true) public String message; @JsonProperty public JSONObject data; public JsonRpcError() { } public JsonRpcError(ErrorCode code, String message, @Nullable JSONObject data) { this.code = code; this.message = message; this.data = data; } public enum ErrorCode { PARSER_ERROR(-32700), INVALID_REQUEST(-32600), METHOD_NOT_FOUND(-32601), INVALID_PARAMS(-32602), INTERNAL_ERROR(-32603); private final int mProtocolValue; private ErrorCode(int protocolValue) { mProtocolValue = protocolValue; } @JsonValue public int getProtocolValue() { return mProtocolValue; } } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/jsonrpc/protocol/JsonRpcEvent.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.jsonrpc.protocol; import javax.annotation.Nullable; import android.annotation.SuppressLint; import com.facebook.stetho.json.annotation.JsonProperty; import org.json.JSONObject; @SuppressLint({ "UsingDefaultJsonDeserializer", "EmptyJsonPropertyUse" }) public class JsonRpcEvent { @JsonProperty(required = true) public String method; @JsonProperty public JSONObject params; public JsonRpcEvent() { } public JsonRpcEvent(String method, @Nullable JSONObject params) { this.method = method; this.params = params; } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/jsonrpc/protocol/JsonRpcRequest.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.jsonrpc.protocol; import android.annotation.SuppressLint; import com.facebook.stetho.json.annotation.JsonProperty; import org.json.JSONObject; @SuppressLint({ "UsingDefaultJsonDeserializer", "EmptyJsonPropertyUse" }) public class JsonRpcRequest { /** * This field is not required so that we can support JSON-RPC "notification" requests. */ @JsonProperty public Long id; @JsonProperty(required = true) public String method; @JsonProperty public JSONObject params; public JsonRpcRequest() { } public JsonRpcRequest(Long id, String method, JSONObject params) { this.id = id; this.method = method; this.params = params; } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/jsonrpc/protocol/JsonRpcResponse.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.jsonrpc.protocol; import android.annotation.SuppressLint; import com.facebook.stetho.json.annotation.JsonProperty; import org.json.JSONObject; @SuppressLint({ "UsingDefaultJsonDeserializer", "EmptyJsonPropertyUse" }) public class JsonRpcResponse { @JsonProperty(required = true) public long id; @JsonProperty public JSONObject result; @JsonProperty public JSONObject error; } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/network/AsyncPrettyPrinter.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.network; import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; /** * Interface that callers need to implement in order to pretty print binary payload received by Stetho */ public interface AsyncPrettyPrinter { /** * Prints the prettified version of payload to output. This method can block * for a certain period of time. Note that Stetho may impose arbitrary * time out on this method. * * @param output Writes the prettified version of payload * @param payload Response stream that has the raw data to be prettified * @throws IOException */ public void printTo(PrintWriter output, InputStream payload) throws IOException; /** * Specifies the type of pretty printed content. Note that this method is called * before the content is actually pretty printed. Stetho uses this * method to make a hopeful guess of the type of prettified content * * @return an enum defined by PrettyPrinterDisplayType class */ public PrettyPrinterDisplayType getPrettifiedType(); } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/network/AsyncPrettyPrinterExecutorHolder.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.network; import javax.annotation.Nullable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * A holder class for the executor service used for pretty printing related tasks */ final class AsyncPrettyPrinterExecutorHolder { private static ExecutorService sExecutorService; private AsyncPrettyPrinterExecutorHolder() { } public static void ensureInitialized() { if (sExecutorService == null) { sExecutorService = Executors.newCachedThreadPool(); } } @Nullable public static ExecutorService getExecutorService() { return sExecutorService; } public static void shutdown() { sExecutorService.shutdown(); sExecutorService = null; } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/network/AsyncPrettyPrinterFactory.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.network; import com.facebook.stetho.inspector.protocol.module.Page; /** * Interface for creating a factory for asynchronous pretty printers */ public interface AsyncPrettyPrinterFactory { /** * Creates an asynchronous pretty printer. This method must not be blocking. * * @param headerName header name of a response which is used to associate * with an asynchronous pretty printer * @param headerValue header value of a response which contains the URI for * the schema data needed to pretty print the response body * @return an asynchronous pretty printer to prettify the response body */ public AsyncPrettyPrinter getInstance(String headerName, String headerValue); } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/network/AsyncPrettyPrinterInitializer.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.network; /** * Interface that is called if AsyncPrettyPrinterRegistry is unpopulated when * the first peer connects to Stetho. It is responsible for registering header * names and their corresponding pretty printers */ public interface AsyncPrettyPrinterInitializer { /** * Populates AsyncPrettyPrinterRegistry with header names and their corresponding pretty * printers. This is responsible for registering all {@link AsyncPrettyPrinter} to the * provided registry. * @param registry */ void populatePrettyPrinters(AsyncPrettyPrinterRegistry registry); } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/network/AsyncPrettyPrinterRegistry.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.network; import javax.annotation.Nullable; import javax.annotation.concurrent.ThreadSafe; import java.util.Collections; import java.util.HashMap; import java.util.Map; @ThreadSafe public class AsyncPrettyPrinterRegistry { private final Map mRegistry = new HashMap<>(); public synchronized void register(String headerName, AsyncPrettyPrinterFactory factory) { mRegistry.put(headerName, factory); } @Nullable public synchronized AsyncPrettyPrinterFactory lookup(String headerName) { return mRegistry.get(headerName); } public synchronized boolean unregister(String headerName) { return mRegistry.remove(headerName) != null; } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/network/CountingOutputStream.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.network; import java.io.FilterOutputStream; import java.io.IOException; import java.io.OutputStream; class CountingOutputStream extends FilterOutputStream { private long mCount; public CountingOutputStream(OutputStream out) { super(out); } public long getCount() { return mCount; } @Override public void write(int oneByte) throws IOException { out.write(oneByte); mCount++; } @Override public void write(byte[] buffer) throws IOException { write(buffer, 0, buffer.length); } @Override public void write(byte[] buffer, int offset, int length) throws IOException { out.write(buffer, offset, length); mCount += length; } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/network/DecompressionHelper.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.network; import com.facebook.stetho.inspector.console.CLog; import com.facebook.stetho.inspector.protocol.module.Console; import javax.annotation.Nullable; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.zip.InflaterOutputStream; // @VisibleForTest public class DecompressionHelper { static final String GZIP_ENCODING = "gzip"; static final String DEFLATE_ENCODING = "deflate"; public static InputStream teeInputWithDecompression( NetworkPeerManager peerManager, String requestId, InputStream availableInputStream, OutputStream decompressedOutput, @Nullable String contentEncoding, ResponseHandler responseHandler) throws IOException { OutputStream output = decompressedOutput; CountingOutputStream decompressedCounter = null; if (contentEncoding != null) { boolean gzipEncoding = GZIP_ENCODING.equals(contentEncoding); boolean deflateEncoding = DEFLATE_ENCODING.equals(contentEncoding); if (gzipEncoding || deflateEncoding) { decompressedCounter = new CountingOutputStream(decompressedOutput); if (gzipEncoding) { output = GunzippingOutputStream.create(decompressedCounter); } else if (deflateEncoding) { output = new InflaterOutputStream(decompressedCounter); } } else { CLog.writeToConsole( peerManager, Console.MessageLevel.WARNING, Console.MessageSource.NETWORK, "Unsupported Content-Encoding in response for request #" + requestId + ": " + contentEncoding); } } return new ResponseHandlingInputStream( availableInputStream, requestId, output, decompressedCounter, peerManager, responseHandler); } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/network/DefaultResponseHandler.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.network; import java.io.IOException; /** * Simple interceptor that delegates response read events to {@link NetworkEventReporter}. */ public class DefaultResponseHandler implements ResponseHandler { private final NetworkEventReporter mEventReporter; private final String mRequestId; private int mBytesRead = 0; private int mDecodedBytesRead = -1; public DefaultResponseHandler(NetworkEventReporter eventReporter, String requestId) { mEventReporter = eventReporter; mRequestId = requestId; } @Override public void onRead(int numBytes) { mBytesRead += numBytes; } @Override public void onReadDecoded(int numBytes) { if (mDecodedBytesRead == -1) { mDecodedBytesRead = 0; } mDecodedBytesRead += numBytes; } public void onEOF() { reportDataReceived(); mEventReporter.responseReadFinished(mRequestId); } public void onError(IOException e) { reportDataReceived(); mEventReporter.responseReadFailed(mRequestId, e.toString()); } private void reportDataReceived() { mEventReporter.dataReceived( mRequestId, mBytesRead, mDecodedBytesRead >= 0 ? mDecodedBytesRead : mBytesRead); } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/network/DownloadingAsyncPrettyPrinterFactory.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.network; import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import com.facebook.stetho.common.ExceptionUtil; import com.facebook.stetho.common.Util; import javax.annotation.Nullable; /** * Abstract class for pretty printer factory that asynchronously downloads schema needed for * pretty printing the payload */ public abstract class DownloadingAsyncPrettyPrinterFactory implements AsyncPrettyPrinterFactory { @Override public AsyncPrettyPrinter getInstance(final String headerName, final String headerValue) { final MatchResult result = matchAndParseHeader(headerName, headerValue); if (result == null) { return null; } String uri = result.getSchemaUri(); URL schemaURL = parseURL(uri); if (schemaURL == null) { return getErrorAsyncPrettyPrinter(headerName, headerValue); } else { ExecutorService executorService = AsyncPrettyPrinterExecutorHolder.getExecutorService(); if (executorService == null) { //last peer is unregistered... return null; } final Future response = executorService.submit(new Request(schemaURL)); return new AsyncPrettyPrinter() { public void printTo(PrintWriter output, InputStream payload) throws IOException { try { String schema; try { schema = response.get(); } catch (ExecutionException e) { Throwable cause = e.getCause(); if (IOException.class.isInstance(cause)) { doErrorPrint( output, payload, "Cannot successfully download schema: " + e.getMessage()); return; } else { throw e; } } doPrint(output, payload, schema); } catch (InterruptedException e) { doErrorPrint( output, payload, "Encountered spurious interrupt while downloading schema for pretty printing: " + e.getMessage()); } catch (ExecutionException e) { Throwable cause = e.getCause(); throw ExceptionUtil.propagate(cause); } } public PrettyPrinterDisplayType getPrettifiedType() { return result.getDisplayType(); } }; } } /** * Match the correct header that contains information about the schema uri * @param headerName header name of a response that needs to be pretty printed * @param headerValue header value which contains the URI for * the schema data needed to pretty print the response body * @return MatchResult that has the schema uri and the type of prettified result. Null * if there is no correct header match. */ @Nullable protected abstract MatchResult matchAndParseHeader(String headerName, String headerValue); /** * Note that the IOException thrown by this method will be propagated all the way up and * yield an error to the chrome devtools */ protected abstract void doPrint(PrintWriter output, InputStream payload, String schema) throws IOException; @Nullable private static URL parseURL(String uri) { try { return new URL(uri); } catch (MalformedURLException e) { return null; } } private static void doErrorPrint(PrintWriter output, InputStream payload, String errorMessage) throws IOException { output.print(errorMessage + "\n" + Util.readAsUTF8(payload)); } private static AsyncPrettyPrinter getErrorAsyncPrettyPrinter( final String headerName, final String headerValue) { return new AsyncPrettyPrinter() { @Override public void printTo(PrintWriter output, InputStream payload) throws IOException { String errorMessage = "[Failed to parse header: " + headerName + " : " + headerValue + " ]"; doErrorPrint(output, payload, errorMessage); } @Override public PrettyPrinterDisplayType getPrettifiedType() { return PrettyPrinterDisplayType.TEXT; } }; } protected class MatchResult { private final String mSchemaUri; private final PrettyPrinterDisplayType mDisplayType; public MatchResult(String schemaUri, PrettyPrinterDisplayType displayType) { mSchemaUri = schemaUri; mDisplayType = displayType; } public String getSchemaUri() { return mSchemaUri; } public PrettyPrinterDisplayType getDisplayType() { return mDisplayType; } } private static class Request implements Callable { private URL url; public Request(URL url) { this.url = url; } @Override public String call() throws IOException { HttpURLConnection connection = (HttpURLConnection)url.openConnection(); int statusCode = connection.getResponseCode(); if (statusCode != 200) { throw new IOException("Got status code: " + statusCode + " while downloading " + "schema with url: " + url.toString()); } InputStream urlStream = connection.getInputStream(); try { return Util.readAsUTF8(urlStream); } finally { urlStream.close(); } } } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/network/GunzippingOutputStream.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.network; import com.facebook.stetho.common.ExceptionUtil; import com.facebook.stetho.common.Util; import java.io.FilterOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PipedInputStream; import java.io.PipedOutputStream; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.zip.GZIPInputStream; /** * An {@link OutputStream} filter which decompresses gzip data before it is written to the * specified destination output stream. This is functionally equivalent to * {@link java.util.zip.InflaterOutputStream} but provides gzip header awareness. The * implementation however is very different to avoid actually interpreting the gzip header. */ class GunzippingOutputStream extends FilterOutputStream { private final Future mCopyFuture; private static final ExecutorService sExecutor = Executors.newCachedThreadPool(); public static GunzippingOutputStream create(OutputStream finalOut) throws IOException { PipedInputStream pipeIn = new PipedInputStream(); PipedOutputStream pipeOut = new PipedOutputStream(pipeIn); Future copyFuture = sExecutor.submit( new GunzippingCallable(pipeIn, finalOut)); return new GunzippingOutputStream(pipeOut, copyFuture); } private GunzippingOutputStream(OutputStream out, Future copyFuture) throws IOException { super(out); mCopyFuture = copyFuture; } @Override public void close() throws IOException { boolean success = false; try { super.close(); success = true; } finally { try { getAndRethrow(mCopyFuture); } catch (IOException e) { if (success) { throw e; } } } } private static T getAndRethrow(Future future) throws IOException { while (true) { try { return future.get(); } catch (InterruptedException e) { // Continue... } catch (ExecutionException e) { Throwable cause = e.getCause(); ExceptionUtil.propagateIfInstanceOf(cause, IOException.class); ExceptionUtil.propagate(cause); } } } private static class GunzippingCallable implements Callable { private final InputStream mIn; private final OutputStream mOut; public GunzippingCallable(InputStream in, OutputStream out) { mIn = in; mOut = out; } @Override public Void call() throws IOException { GZIPInputStream in = new GZIPInputStream(mIn); try { Util.copy(in, mOut, new byte[1024]); } finally { in.close(); mOut.close(); } return null; } } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/network/MimeMatcher.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.network; import javax.annotation.Nullable; import java.util.ArrayList; import android.annotation.SuppressLint; public class MimeMatcher { private final ArrayList mRuleMap = new ArrayList(); /** * Add a matching rule in the canonical MIME T form such as "image/*" or a MIME T * literal such as "text/html". * * @param ruleExpression Expression to match, in the order it was added. * @param resultIfMatched Result if this expression matches. */ public void addRule(String ruleExpression, T resultIfMatched) { mRuleMap.add(new MimeMatcherRule(ruleExpression, resultIfMatched)); } public void clear() { mRuleMap.clear(); } @Nullable public T match(String mimeT) { int ruleMapN = mRuleMap.size(); for (int i = 0; i < ruleMapN; i++) { MimeMatcherRule rule = mRuleMap.get(i); if (rule.match(mimeT)) { return rule.getResultIfMatched(); } } return null; } @SuppressLint("BadMethodUse-java.lang.String.length") private class MimeMatcherRule { private final boolean mHasWildcard; private final String mMatchPrefix; private final T mResultIfMatched; public MimeMatcherRule(String ruleExpression, T resultIfMatched) { if (ruleExpression.endsWith("*")) { mHasWildcard = true; mMatchPrefix = ruleExpression.substring(0, ruleExpression.length() - 1); } else { mHasWildcard = false; mMatchPrefix = ruleExpression; } if (mMatchPrefix.contains("*")) { throw new IllegalArgumentException("Multiple wildcards present in rule expression " + ruleExpression); } mResultIfMatched = resultIfMatched; } public boolean match(String mimeType) { // Make sure the string literal matches. if (!mimeType.startsWith(mMatchPrefix)) { return false; } // If we have a wildcard and the prefix matches, then we've matched; otherwise if the // string literal and the mime T are the same length then we must have a match. return (mHasWildcard || mimeType.length() == mMatchPrefix.length()); } public T getResultIfMatched() { return mResultIfMatched; } } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/network/NetworkEventReporter.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.network; import javax.annotation.Nullable; import java.io.IOException; import java.io.InputStream; /** * Interface that callers must invoke in order to supply data to the Network tab in * the WebKit Inspector. For HTTP specific traffic, the following call flow must be met: * *
 * requestWillBeSent +---> responseHeadersReceived +---> interpretResponseStream
 *                   |           |                 |
 *                   |           `---> dataSent    |
 *                   |                             |
 *                   `-----------------------------`--------> httpExchangeFailed
 * 
* *

Note that {@link #interpretResponseStream} combined with {@link DefaultResponseHandler} * will automatically invoke {@link #dataReceived}, {@link #responseReadFailed} and * {@link #responseReadFinished}. If you use your own custom {@link ResponseHandler} you * must be sure to invoke these methods manually.

* *

For arbitrary sockets or explicitly for WebSockets, the following call flow must be met:

* *
 * webSocketCreated +---> webSocketWillSendHandshakeRequest ----> webSocketHandshakeResponseReceived
 *                  |                                                              |
 *                  |                     ,----------------------+-----------------+--------,
 *                  |                     v                      v                          |
 *                  +---------> [ webSocketFrameSent | webSocketFrameReceived ] ---,        |
 *                  |                     ^                      ^                 |        |
 *                  |                     `----------------------+-----------------+        |
 *                  |                                                              |        |
 *                  `---------> webSocketClosed <----------------------------------'        |
 *                                     ^                                                    |
 *                                     `----------------------------------------------------'
 * 
* *

Explicitly worth nothing is that regular sockets in an Android app can be treated as * WebSockets for the purpose of arbitrary socket inspection and can skip * {@link #webSocketWillSendHandshakeRequest} and {@link #webSocketHandshakeResponseReceived} * which are only used for the WebSocket-specific HTTP upgrade.

*/ public interface NetworkEventReporter { /** * Returns true if there is at least one peer listening for network events; false otherwise. * This value is provided as an optimization to avoid expensive work when the WebKit Inspector is * not being used. It is otherwise safe to invoke methods defined in this interface when * the value is false. */ boolean isEnabled(); /** * Indicates that a request is about to be sent, but has not yet been delivered over the wire. * * @param request Request descriptor. */ void requestWillBeSent(InspectorRequest request); /** * Indicates that a response message was just received from the network, but the body * has not yet been read. * * @param response Response descriptor. */ void responseHeadersReceived(InspectorResponse response); /** * Indicates that communication with the server has failed. You are expected to call this for any * exception before you call {@link #interpretResponseStream}. After * {@link #interpretResponseStream} is called we will reporting any * {@link IOException} during reading from the {@link InputStream}. * * @param requestId Unique identifier for the request as per {@link InspectorRequest#id()} * @param errorText Text to report for the error; using {@link IOException#toString()} is * recommended. */ void httpExchangeFailed(String requestId, String errorText); /** * Intercept the stream as given by the underlying HTTP library that contains the body of the * response. In order to have the response show up in inspector (and to have the request be * completed successfully) you need to call this AND read until exhaustion/EOF of the returned * stream. * *

* We will internally signal a failure if there is an {@link IOException} received while reading * from the stream. * *

* Do not invoke {@link #httpExchangeFailed(String, String)} after calling this method. * * @param requestId Unique identifier for the request as per {@link InspectorRequest#id()} * @param contentType The {@code Content-Type} header value that was specified in * {@link InspectorResponse}. This header is used to determine the appropriate * storage format for the body. For instance, {@code image/*} is necessary to cause * images to appear in the Inspector UI. * @param contentEncoding The {@code Content-Encoding} header value that was specified in * {@link InspectorResponse}. This header is used to determine what type of decompression * is to be applied when delivering the raw response stream to the debugging interface. * If null, no decompression will be used. * @param inputStream Response stream if applicable ("HEAD" for instance does not have a body). * {@code null} otherwise. * @param responseHandler Callback to forward stream events back to the relevant event reporter * methods. Recommend using {@link DefaultResponseHandler} for most callers. * * @return {@link InputStream} that has been intercepted if WebkitInspector is active and enabled * otherwise it will return {@code inputStream} */ @Nullable InputStream interpretResponseStream( String requestId, @Nullable String contentType, @Nullable String contentEncoding, @Nullable InputStream inputStream, ResponseHandler responseHandler); /** * Indicates that there was a failure while reading from response stream. If you use * {@link #interpretResponseStream} with {@link DefaultResponseHandler} (as is recommended), * this method will be invoked automatically for you. * * @param requestId Unique identifier for the request as per {@link InspectorRequest#id()} * @param errorText Text to report for the error; using {@link IOException#toString()} is * recommended. */ void responseReadFailed(String requestId, String errorText); /** * Indicates that the response stream has been fully exhausted and the request is now * complete. If you use {@link #interpretResponseStream} with {@link DefaultResponseHandler} * (as is recommended), this method will be invoked automatically for you. * * @param requestId Unique identifier for the request as per {@link InspectorRequest#id()} */ void responseReadFinished(String requestId); /** * Indicates that raw data was sent over the network. It is permissible to invoke this * method just once after the full size of the request is known. *

* Invoking this method is optional and merely provides additional timing metrics and actual * payload sizes to the Inspector UI. * * @param requestId Unique identifier for the request as per {@link InspectorRequest#id()} * @param dataLength Uncompressed data segment length * @param encodedDataLength Compressed data segment length */ void dataSent(String requestId, int dataLength, int encodedDataLength); /** * Indicates that raw data was received from the network. * * @see #dataSent */ void dataReceived(String requestId, int dataLength, int encodedDataLength); /** * Provides unique request id for {@link InspectorRequest#id()}. */ String nextRequestId(); /** * Invoked when a socket is created and implicitly being connected (but not necessarily connected * yet). If a websocket is being used, proceed to {@link #webSocketWillSendHandshakeRequest}. * Otherwise you may proceed next to {@code webSocketFrame*} methods. */ void webSocketCreated(String requestId, String url); /** * Socket has been closed for unknown reasons. Consider first invoking * {@link #webSocketFrameError} even for standard sockets to provide context. */ void webSocketClosed(String requestId); /** * Invoked specifically for websockets to communicate the WebSocket upgrade messages. Not * necessary to call for standard sockets. */ void webSocketWillSendHandshakeRequest(InspectorWebSocketRequest request); /** * Delivers the reply from the peer in response to the WebSocket upgrade request. */ void webSocketHandshakeResponseReceived(InspectorWebSocketResponse response); /** * Send a "websocket" frame from our app to the remote peer. Standard sockets can simply emulate * this by capturing each socket send as a frame of either * {@link InspectorWebSocketFrame#OPCODE_BINARY} or * {@link InspectorWebSocketFrame#OPCODE_TEXT}. Note that binary * payloads are not visualized in Chrome but can be sent by simply assuming the data is UTF-8 * encoded (yes, really: * https://chromium.googlesource.com/chromium/blink/+/master/Source/core/inspector/InspectorResourceAgent.cpp#850). */ void webSocketFrameSent(InspectorWebSocketFrame frame); /** * The receive side of {@link #webSocketFrameSent}. */ void webSocketFrameReceived(InspectorWebSocketFrame frame); /** * Indicates a web socket (or standard socket) error has occurred though this doesn't * explicitly close the socket (see {@link #webSocketClosed}) but it does let the UI * know that it should denote the closure as forceful or as a failure in some way. */ void webSocketFrameError(String requestId, String errorMessage); /** * Represents the request that will be sent over HTTP. Note that for many implementations * of HTTP the request constructed may differ from the request actually sent over the wire. * For instance, additional headers like {@code Host}, {@code User-Agent}, {@code Content-Type}, * etc may not be part of this request but should be injected if necessary. Some stacks offer * inspection of the raw request about to be sent to the server which is preferable. */ interface InspectorRequest extends InspectorRequestCommon { /** * Provide an extra integer to decorate the {@link #friendlyName()}. This shows up next to * it in the WebKit Inspector UI and can be used to indicate things like request priority. */ @Nullable Integer friendlyNameExtra(); String url(); /** * HTTP method ("GET", "POST", "DELETE", etc). */ String method(); /** * Provide the body if part of an entity-enclosing request (like "POST" or "PUT"). May * return null otherwise. */ @Nullable byte[] body() throws IOException; } interface InspectorResponse extends InspectorResponseCommon { String url(); /** * True if the response was furnished on a re-used socket; false otherwise or if unknown. */ boolean connectionReused(); /** * Unique connection identifier representing the socket that was used to furnish the response. */ int connectionId(); /** * True if the response was furnished by disk cache; false otherwise or if unknown. */ boolean fromDiskCache(); } interface InspectorWebSocketRequest extends InspectorRequestCommon { } interface InspectorWebSocketResponse extends InspectorResponseCommon { /** * Optional and redundant set of headers from {@link InspectorWebSocketRequest} that are for * some mysterious reason are included here in the response by Chrome. */ @Nullable InspectorHeaders requestHeaders(); } interface InspectorWebSocketFrame { String requestId(); int OPCODE_CONTINUATION = 0; int OPCODE_TEXT = 1; int OPCODE_BINARY = 2; int OPCODE_CONNECTION_CLOSE = 8; int OPCODE_PING = 9; int OPCODE_PONG = 10; int opcode(); boolean mask(); String payloadData(); } interface InspectorRequestCommon extends InspectorHeaders { /** * Unique identifier for this request. This identifier must be used in all other network * events corresponding to this request. Identifiers may be re-used for HTTP requests or * WebSockets that have exhuasted the state machine to its final closed/finished state. */ String id(); /** * Arbitrary debug-friendly name of the request. */ String friendlyName(); } interface InspectorResponseCommon extends InspectorHeaders { /** @see InspectorRequest#id() */ String requestId(); int statusCode(); String reasonPhrase(); } interface InspectorHeaders { int headerCount(); String headerName(int index); String headerValue(int index); @Nullable String firstHeaderValue(String name); } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/network/NetworkEventReporterImpl.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.network; import android.os.SystemClock; import com.facebook.stetho.common.Utf8Charset; import com.facebook.stetho.inspector.console.CLog; import com.facebook.stetho.inspector.protocol.module.Console; import com.facebook.stetho.inspector.protocol.module.Network; import com.facebook.stetho.inspector.protocol.module.Page; import org.json.JSONException; import org.json.JSONObject; import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.concurrent.atomic.AtomicInteger; /** * Implementation of {@link NetworkEventReporter} which allows callers to inform the Stetho * system of network traffic. Callers can safely eagerly access this class and store a * reference if they wish. When WebKit Inspector clients are connected, the internal * implementation will be automatically wired up to them. */ public class NetworkEventReporterImpl implements NetworkEventReporter { private final AtomicInteger mNextRequestId = new AtomicInteger(0); @Nullable private ResourceTypeHelper mResourceTypeHelper; private static NetworkEventReporter sInstance; private NetworkEventReporterImpl() { } /** * Static accessor allowing callers to easily hook into the WebKit Inspector system without * creating dependencies on the main Stetho initialization code path. */ public static synchronized NetworkEventReporter get() { if (sInstance == null) { sInstance = new NetworkEventReporterImpl(); } return sInstance; } @Override public boolean isEnabled() { NetworkPeerManager peerManager = getPeerManagerIfEnabled(); return peerManager != null; } @Nullable private NetworkPeerManager getPeerManagerIfEnabled() { NetworkPeerManager peerManager = NetworkPeerManager.getInstanceOrNull(); if (peerManager != null && peerManager.hasRegisteredPeers()) { return peerManager; } return null; } @Override public void requestWillBeSent(InspectorRequest request) { NetworkPeerManager peerManager = getPeerManagerIfEnabled(); if (peerManager != null) { Network.Request requestJSON = new Network.Request(); requestJSON.url = request.url(); requestJSON.method = request.method(); requestJSON.headers = formatHeadersAsJSON(request); requestJSON.postData = readBodyAsString(peerManager, request); // Hack to use the initiator of SCRIPT to generate a fake call stack that includes // the request's "friendly" name. String requestFriendlyName = request.friendlyName(); Integer requestPriority = request.friendlyNameExtra(); Network.Initiator initiatorJSON = new Network.Initiator(); initiatorJSON.type = Network.InitiatorType.SCRIPT; initiatorJSON.stackTrace = new ArrayList(); initiatorJSON.stackTrace.add(new Console.CallFrame(requestFriendlyName, requestFriendlyName, requestPriority != null ? requestPriority : 0 /* lineNumber */, 0 /* columnNumber */)); Network.RequestWillBeSentParams params = new Network.RequestWillBeSentParams(); params.requestId = request.id(); params.frameId = "1"; params.loaderId = "1"; params.documentURL = request.url(); params.request = requestJSON; params.timestamp = stethoNow() / 1000.0; params.initiator = initiatorJSON; params.redirectResponse = null; // Type is now required as of at least WebKit Inspector rev @188492. If you don't send // it, Chrome will refuse to draw the row in the Network tab until the response is // received (providing the type). This delay is very noticable on slow networks. params.type = Page.ResourceType.OTHER; peerManager.sendNotificationToPeers("Network.requestWillBeSent", params); } } @Nullable private static String readBodyAsString( NetworkPeerManager peerManager, InspectorRequest request) { try { byte[] body = request.body(); if (body != null) { return new String(body, Utf8Charset.INSTANCE); } } catch (IOException | OutOfMemoryError e) { CLog.writeToConsole( peerManager, Console.MessageLevel.WARNING, Console.MessageSource.NETWORK, "Could not reproduce POST body: " + e); } return null; } @Override public void responseHeadersReceived(InspectorResponse response) { NetworkPeerManager peerManager = getPeerManagerIfEnabled(); if (peerManager != null) { Network.Response responseJSON = new Network.Response(); responseJSON.url = response.url(); responseJSON.status = response.statusCode(); responseJSON.statusText = response.reasonPhrase(); responseJSON.headers = formatHeadersAsJSON(response); String contentType = getContentType(response); responseJSON.mimeType = contentType != null ? getResourceTypeHelper().stripContentExtras(contentType) : "application/octet-stream"; responseJSON.connectionReused = response.connectionReused(); responseJSON.connectionId = response.connectionId(); responseJSON.fromDiskCache = response.fromDiskCache(); Network.ResponseReceivedParams receivedParams = new Network.ResponseReceivedParams(); receivedParams.requestId = response.requestId(); receivedParams.frameId = "1"; receivedParams.loaderId = "1"; receivedParams.timestamp = stethoNow() / 1000.0; receivedParams.response = responseJSON; AsyncPrettyPrinter asyncPrettyPrinter = initAsyncPrettyPrinterForResponse(response, peerManager); receivedParams.type = determineResourceType(asyncPrettyPrinter, contentType, getResourceTypeHelper()); peerManager.sendNotificationToPeers("Network.responseReceived", receivedParams); } } @Nullable private static AsyncPrettyPrinter initAsyncPrettyPrinterForResponse( InspectorResponse response, NetworkPeerManager peerManager) { AsyncPrettyPrinterRegistry registry = peerManager.getAsyncPrettyPrinterRegistry(); AsyncPrettyPrinter asyncPrettyPrinter = createPrettyPrinterForResponse(response, registry); if (asyncPrettyPrinter != null) { peerManager.getResponseBodyFileManager().associateAsyncPrettyPrinterWithId( response.requestId(), asyncPrettyPrinter); } return asyncPrettyPrinter; } private static Page.ResourceType determineResourceType( AsyncPrettyPrinter asyncPrettyPrinter, String contentType, ResourceTypeHelper resourceTypeHelper) { if (asyncPrettyPrinter != null) { return asyncPrettyPrinter.getPrettifiedType().getResourceType(); } else { return contentType != null ? resourceTypeHelper.determineResourceType(contentType) : Page.ResourceType.OTHER; } } //@VisibleForTesting @Nullable static AsyncPrettyPrinter createPrettyPrinterForResponse( InspectorResponse response, @Nullable AsyncPrettyPrinterRegistry registry) { if (registry != null) { for (int i = 0, count = response.headerCount(); i < count; i++) { AsyncPrettyPrinterFactory factory = registry.lookup(response.headerName(i)); if (factory != null) { AsyncPrettyPrinter asyncPrettyPrinter = factory.getInstance( response.headerName(i), response.headerValue(i)); return asyncPrettyPrinter; } } } return null; } @Override public InputStream interpretResponseStream( String requestId, @Nullable String contentType, @Nullable String contentEncoding, @Nullable InputStream availableInputStream, ResponseHandler responseHandler) { NetworkPeerManager peerManager = getPeerManagerIfEnabled(); if (peerManager != null) { if (availableInputStream == null) { responseHandler.onEOF(); return null; } Page.ResourceType resourceType = contentType != null ? getResourceTypeHelper().determineResourceType(contentType) : null; // There's this weird logic at play that only knows how to base64 decode certain kinds of // resources. boolean base64Encode = false; if (resourceType != null && resourceType == Page.ResourceType.IMAGE) { base64Encode = true; } try { OutputStream fileOutputStream = peerManager.getResponseBodyFileManager().openResponseBodyFile( requestId, base64Encode); return DecompressionHelper.teeInputWithDecompression( peerManager, requestId, availableInputStream, fileOutputStream, contentEncoding, responseHandler); } catch (IOException e) { CLog.writeToConsole( peerManager, Console.MessageLevel.ERROR, Console.MessageSource.NETWORK, "Error writing response body data for request #" + requestId); } } return availableInputStream; } @Override public void httpExchangeFailed(String requestId, String errorText) { loadingFailed(requestId, errorText); } @Override public void responseReadFinished(String requestId) { loadingFinished(requestId); } private void loadingFinished(String requestId) { NetworkPeerManager peerManager = getPeerManagerIfEnabled(); if (peerManager != null) { Network.LoadingFinishedParams finishedParams = new Network.LoadingFinishedParams(); finishedParams.requestId = requestId; finishedParams.timestamp = stethoNow() / 1000.0; peerManager.sendNotificationToPeers("Network.loadingFinished", finishedParams); } } @Override public void responseReadFailed(String requestId, String errorText) { loadingFailed(requestId, errorText); } private void loadingFailed(String requestId, String errorText) { NetworkPeerManager peerManager = getPeerManagerIfEnabled(); if (peerManager != null) { Network.LoadingFailedParams failedParams = new Network.LoadingFailedParams(); failedParams.requestId = requestId; failedParams.timestamp = stethoNow() / 1000.0; failedParams.errorText = errorText; failedParams.type = Page.ResourceType.OTHER; peerManager.sendNotificationToPeers("Network.loadingFailed", failedParams); } } @Override public void dataSent( String requestId, int dataLength, int encodedDataLength) { // The inspector protocol only gives us the dataReceived event, but we can happily combine // upstream and downstream data into this to visualize the real size of the request, not // strictly the size of the "content" as reported in the UI. dataReceived(requestId, dataLength, encodedDataLength); } @Override public void dataReceived( String requestId, int dataLength, int encodedDataLength) { NetworkPeerManager peerManager = getPeerManagerIfEnabled(); if (peerManager != null) { Network.DataReceivedParams dataReceivedParams = new Network.DataReceivedParams(); dataReceivedParams.requestId = requestId; dataReceivedParams.timestamp = stethoNow() / 1000.0; dataReceivedParams.dataLength = dataLength; dataReceivedParams.encodedDataLength = encodedDataLength; peerManager.sendNotificationToPeers("Network.dataReceived", dataReceivedParams); } } @Override public String nextRequestId() { return String.valueOf(mNextRequestId.getAndIncrement()); } @Nullable private String getContentType(InspectorHeaders headers) { // This may need to change in the future depending on how cumbersome header simulation // is for the various hooks we expose. return headers.firstHeaderValue("Content-Type"); } @Override public void webSocketCreated(String requestId, String url) { NetworkPeerManager peerManager = getPeerManagerIfEnabled(); if (peerManager != null) { Network.WebSocketCreatedParams params = new Network.WebSocketCreatedParams(); params.requestId = requestId; params.url = url; peerManager.sendNotificationToPeers("Network.webSocketCreated", params); } } @Override public void webSocketClosed(String requestId) { NetworkPeerManager peerManager = getPeerManagerIfEnabled(); if (peerManager != null) { Network.WebSocketClosedParams params = new Network.WebSocketClosedParams(); params.requestId = requestId; params.timestamp = stethoNow() / 1000.0; peerManager.sendNotificationToPeers("Network.webSocketClosed", params); } } @Override public void webSocketWillSendHandshakeRequest(InspectorWebSocketRequest request) { NetworkPeerManager peerManager = getPeerManagerIfEnabled(); if (peerManager != null) { Network.WebSocketWillSendHandshakeRequestParams params = new Network.WebSocketWillSendHandshakeRequestParams(); params.requestId = request.id(); params.timestamp = stethoNow() / 1000.0; params.wallTime = System.currentTimeMillis() / 1000.0; Network.WebSocketRequest requestJSON = new Network.WebSocketRequest(); requestJSON.headers = formatHeadersAsJSON(request); params.request = requestJSON; peerManager.sendNotificationToPeers("Network.webSocketWillSendHandshakeRequest", params); } } @Override public void webSocketHandshakeResponseReceived(InspectorWebSocketResponse response) { NetworkPeerManager peerManager = getPeerManagerIfEnabled(); if (peerManager != null) { Network.WebSocketHandshakeResponseReceivedParams params = new Network.WebSocketHandshakeResponseReceivedParams(); params.requestId = response.requestId(); params.timestamp = stethoNow() / 1000.0; Network.WebSocketResponse responseJSON = new Network.WebSocketResponse(); responseJSON.headers = formatHeadersAsJSON(response); responseJSON.headersText = null; if (response.requestHeaders() != null) { responseJSON.requestHeaders = formatHeadersAsJSON(response.requestHeaders()); responseJSON.requestHeadersText = null; } responseJSON.status = response.statusCode(); responseJSON.statusText = response.reasonPhrase(); params.response = responseJSON; peerManager.sendNotificationToPeers("Network.webSocketHandshakeResponseReceived", params); } } @Override public void webSocketFrameSent(InspectorWebSocketFrame frame) { NetworkPeerManager peerManager = getPeerManagerIfEnabled(); if (peerManager != null) { Network.WebSocketFrameSentParams params = new Network.WebSocketFrameSentParams(); params.requestId = frame.requestId(); params.timestamp = stethoNow() / 1000.0; params.response = convertFrame(frame); peerManager.sendNotificationToPeers("Network.webSocketFrameSent", params); } } @Override public void webSocketFrameReceived(InspectorWebSocketFrame frame) { NetworkPeerManager peerManager = getPeerManagerIfEnabled(); if (peerManager != null) { Network.WebSocketFrameReceivedParams params = new Network.WebSocketFrameReceivedParams(); params.requestId = frame.requestId(); params.timestamp = stethoNow() / 1000.0; params.response = convertFrame(frame); peerManager.sendNotificationToPeers("Network.webSocketFrameReceived", params); } } private static Network.WebSocketFrame convertFrame(InspectorWebSocketFrame in) { Network.WebSocketFrame out = new Network.WebSocketFrame(); out.opcode = in.opcode(); out.mask = in.mask(); out.payloadData = in.payloadData(); return out; } @Override public void webSocketFrameError(String requestId, String errorMessage) { NetworkPeerManager peerManager = getPeerManagerIfEnabled(); if (peerManager != null) { Network.WebSocketFrameErrorParams params = new Network.WebSocketFrameErrorParams(); params.requestId = requestId; params.timestamp = stethoNow() / 1000.0; params.errorMessage = errorMessage; peerManager.sendNotificationToPeers("Network.webSocketFrameError", params); } } private static JSONObject formatHeadersAsJSON(InspectorHeaders headers) { JSONObject json = new JSONObject(); for (int i = 0; i < headers.headerCount(); i++) { String name = headers.headerName(i); String value = headers.headerValue(i); try { if (json.has(name)) { // Multiple headers are separated with a new line. json.put(name, json.getString(name) + "\n" + value); } else { json.put(name, value); } } catch (JSONException e) { throw new RuntimeException(e); } } return json; } @Nonnull private ResourceTypeHelper getResourceTypeHelper() { if (mResourceTypeHelper == null) { mResourceTypeHelper = new ResourceTypeHelper(); } return mResourceTypeHelper; } private static long stethoNow() { return SystemClock.elapsedRealtime(); } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/network/NetworkPeerManager.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.network; import javax.annotation.Nullable; import android.content.Context; import com.facebook.stetho.common.Util; import com.facebook.stetho.inspector.helper.ChromePeerManager; import com.facebook.stetho.inspector.helper.PeersRegisteredListener; public class NetworkPeerManager extends ChromePeerManager { private static NetworkPeerManager sInstance; private final ResponseBodyFileManager mResponseBodyFileManager; private AsyncPrettyPrinterInitializer mPrettyPrinterInitializer; private AsyncPrettyPrinterRegistry mAsyncPrettyPrinterRegistry; @Nullable public static synchronized NetworkPeerManager getInstanceOrNull() { return sInstance; } public static synchronized NetworkPeerManager getOrCreateInstance(Context context) { if (sInstance == null) { sInstance = new NetworkPeerManager( new ResponseBodyFileManager( context.getApplicationContext())); } return sInstance; } public NetworkPeerManager( ResponseBodyFileManager responseBodyFileManager) { mResponseBodyFileManager = responseBodyFileManager; setListener(mTempFileCleanup); } public ResponseBodyFileManager getResponseBodyFileManager() { return mResponseBodyFileManager; } @Nullable public AsyncPrettyPrinterRegistry getAsyncPrettyPrinterRegistry() { return mAsyncPrettyPrinterRegistry; } public void setPrettyPrinterInitializer(AsyncPrettyPrinterInitializer initializer) { Util.throwIfNotNull(mPrettyPrinterInitializer); mPrettyPrinterInitializer = Util.throwIfNull(initializer); } private final PeersRegisteredListener mTempFileCleanup = new PeersRegisteredListener() { @Override protected void onFirstPeerRegistered() { AsyncPrettyPrinterExecutorHolder.ensureInitialized(); if (mAsyncPrettyPrinterRegistry == null && mPrettyPrinterInitializer != null) { mAsyncPrettyPrinterRegistry = new AsyncPrettyPrinterRegistry(); mPrettyPrinterInitializer.populatePrettyPrinters(mAsyncPrettyPrinterRegistry); } mResponseBodyFileManager.cleanupFiles(); } @Override protected void onLastPeerUnregistered() { mResponseBodyFileManager.cleanupFiles(); AsyncPrettyPrinterExecutorHolder.shutdown(); } }; } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/network/PrettyPrinterDisplayType.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.network; import com.facebook.stetho.inspector.protocol.module.Page; public enum PrettyPrinterDisplayType { JSON(Page.ResourceType.XHR), HTML(Page.ResourceType.DOCUMENT), TEXT(Page.ResourceType.DOCUMENT); private final Page.ResourceType mResourceType; private PrettyPrinterDisplayType(Page.ResourceType resourceType) { mResourceType = resourceType; } /** * Converts PrettyPrinterDisplayType values to the appropriate * {@link Page.ResourceType} values that Stetho understands */ public Page.ResourceType getResourceType() { return mResourceType; } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/network/RequestBodyHelper.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.network; import javax.annotation.Nullable; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.zip.InflaterOutputStream; /** * Helper which manages provides computed request sizes as well as transparent decompression. * Note that request compression is not officially part of the HTTP standard however it is * commonly in use and can be conveniently supported here. *

* To use, invoke {@link #createBodySink} to prepare an output stream where the raw body can be * written. Then invoke {@link #getDisplayBody()} to retrieve the possibly decoded body. * Finally, {@link #reportDataSent()} can be called to report to Stetho the raw and decompressed * payload sizes. */ public class RequestBodyHelper { private final NetworkEventReporter mEventReporter; private final String mRequestId; private ByteArrayOutputStream mDeflatedOutput; private CountingOutputStream mDeflatingOutput; public RequestBodyHelper(NetworkEventReporter eventReporter, String requestId) { mEventReporter = eventReporter; mRequestId = requestId; } public OutputStream createBodySink(@Nullable String contentEncoding) throws IOException { OutputStream deflatingOutput; ByteArrayOutputStream deflatedOutput = new ByteArrayOutputStream(); if (DecompressionHelper.GZIP_ENCODING.equals(contentEncoding)) { deflatingOutput = GunzippingOutputStream.create(deflatedOutput); } else if (DecompressionHelper.DEFLATE_ENCODING.equals(contentEncoding)) { deflatingOutput = new InflaterOutputStream(deflatedOutput); } else { deflatingOutput = deflatedOutput; } mDeflatingOutput = new CountingOutputStream(deflatingOutput); mDeflatedOutput = deflatedOutput; return mDeflatingOutput; } public byte[] getDisplayBody() { throwIfNoBody(); return mDeflatedOutput.toByteArray(); } public boolean hasBody() { return mDeflatedOutput != null; } public void reportDataSent() { throwIfNoBody(); mEventReporter.dataSent( mRequestId, mDeflatedOutput.size(), (int)mDeflatingOutput.getCount()); } private void throwIfNoBody() { if (!hasBody()) { throw new IllegalStateException("No body found; has createBodySink been called?"); } } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/network/ResourceTypeHelper.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.network; import com.facebook.stetho.inspector.protocol.module.Page; public class ResourceTypeHelper { private final MimeMatcher mMimeMatcher; public ResourceTypeHelper() { mMimeMatcher = new MimeMatcher(); mMimeMatcher.addRule("text/css", Page.ResourceType.STYLESHEET); mMimeMatcher.addRule("image/*", Page.ResourceType.IMAGE); mMimeMatcher.addRule("application/x-javascript", Page.ResourceType.SCRIPT); // This is apparently important to allow the WebKit inspector to do JSON preview. I don't // know exactly why, but whatever. mMimeMatcher.addRule("text/javascript", Page.ResourceType.XHR); mMimeMatcher.addRule("application/json", Page.ResourceType.XHR); // Everything else gets a lame, unformatted blob. mMimeMatcher.addRule("text/*", Page.ResourceType.DOCUMENT); // I think this disables preview. Perhaps that's not what we want as the default but we'll // need some time to soak in real data to see for sure. mMimeMatcher.addRule("*", Page.ResourceType.OTHER); } public Page.ResourceType determineResourceType(String contentType) { String mimeType = stripContentExtras(contentType); return mMimeMatcher.match(mimeType); } /** * Strip out any extra data following the semicolon (e.g. \"text/javascript; charset=UTF-8"). * * @return MIME type with content extras stripped out (if there were any). */ public String stripContentExtras(String contentType) { int index = contentType.indexOf(';'); return (index >= 0) ? contentType.substring(0, index) : contentType; } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/network/ResponseBodyData.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.network; /** * Special file data necessary to comply with the Chrome DevTools instance which doesn't let * us just naively base64 encode everything. */ public class ResponseBodyData { public String data; public boolean base64Encoded; } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/network/ResponseBodyFileManager.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.network; import java.io.ByteArrayOutputStream; import java.io.EOFException; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintWriter; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import android.content.Context; import android.util.Base64; import android.util.Base64OutputStream; import com.facebook.stetho.common.ExceptionUtil; import com.facebook.stetho.common.LogRedirector; import com.facebook.stetho.common.Util; /** * Manages temporary files created by {@link ChromeHttpFlowObserver} to serve request bodies. */ public class ResponseBodyFileManager { private static final String TAG = "ResponseBodyFileManager"; private static final String FILENAME_PREFIX = "network-response-body-"; private static final int PRETTY_PRINT_TIMEOUT_SEC = 10; private final Context mContext; private final Map mRequestIdMap = Collections.synchronizedMap( new HashMap()); public ResponseBodyFileManager(Context context) { mContext = context; } public void cleanupFiles() { for (File file : mContext.getFilesDir().listFiles()) { if (file.getName().startsWith(FILENAME_PREFIX)) { if (!file.delete()) { LogRedirector.w(TAG, "Failed to delete " + file.getAbsolutePath()); } } } LogRedirector.i(TAG, "Cleaned up temporary network files."); } public ResponseBodyData readFile(String requestId) throws IOException { InputStream in = mContext.openFileInput(getFilename(requestId)); try { int firstByte = in.read(); if (firstByte == -1) { throw new EOFException("Failed to read base64Encode byte"); } ResponseBodyData bodyData = new ResponseBodyData(); bodyData.base64Encoded = firstByte != 0; AsyncPrettyPrinter asyncPrettyPrinter = mRequestIdMap.get(requestId); if (asyncPrettyPrinter != null) { // TODO: this line blocks for up to 10 seconds and create problems as described // in issue #243 allow asynchronous dispatch for MethodDispatcher bodyData.data = prettyPrintContentWithTimeOut(asyncPrettyPrinter, in); } else { bodyData.data = Util.readAsUTF8(in); } return bodyData; } finally { in.close(); } } private String prettyPrintContentWithTimeOut( AsyncPrettyPrinter asyncPrettyPrinter, InputStream in) throws IOException { AsyncPrettyPrintingCallable prettyPrintingCallable = new AsyncPrettyPrintingCallable( in, asyncPrettyPrinter); ExecutorService executorService = AsyncPrettyPrinterExecutorHolder.getExecutorService(); if (executorService == null) { //last peer is unregistered... return null; } Future future = executorService.submit(prettyPrintingCallable); try { return Util.getUninterruptibly(future, PRETTY_PRINT_TIMEOUT_SEC, TimeUnit.SECONDS); } catch (TimeoutException e) { future.cancel(true); return "Time out after " + PRETTY_PRINT_TIMEOUT_SEC + " seconds of attempting to pretty print\n" + Util.readAsUTF8(in); } catch (ExecutionException e) { Throwable cause = e.getCause(); ExceptionUtil.propagateIfInstanceOf(cause, IOException.class); throw ExceptionUtil.propagate(cause); } } public OutputStream openResponseBodyFile(String requestId, boolean base64Encode) throws IOException { OutputStream out = mContext.openFileOutput(getFilename(requestId), Context.MODE_PRIVATE); out.write(base64Encode ? 1 : 0); if (base64Encode) { return new Base64OutputStream(out, Base64.DEFAULT); } else { return out; } } private static String getFilename(String requestId) { return FILENAME_PREFIX + requestId; } /** * Associates an asynchronous pretty printer with a response request id * The pretty printer will be used to pretty print the response body that has * the particular request id * * @param requestId Unique identifier for the response * as per {@link NetworkEventReporter.InspectorResponse#requestId()} * @param asyncPrettyPrinter Asynchronous Pretty Printer to pretty print the response body */ public void associateAsyncPrettyPrinterWithId( String requestId, AsyncPrettyPrinter asyncPrettyPrinter) { if (mRequestIdMap.put(requestId, asyncPrettyPrinter) != null) { throw new IllegalArgumentException("cannot associate different " + "pretty printers with the same request id: "+requestId); } } private class AsyncPrettyPrintingCallable implements Callable { private final InputStream mInputStream; private final AsyncPrettyPrinter mAsyncPrettyPrinter; public AsyncPrettyPrintingCallable( InputStream in, AsyncPrettyPrinter asyncPrettyPrinter) { mInputStream = in; mAsyncPrettyPrinter = asyncPrettyPrinter; } @Override public String call() throws IOException { return prettyPrintContent(mInputStream, mAsyncPrettyPrinter); } private String prettyPrintContent(InputStream in, AsyncPrettyPrinter asyncPrettyPrinter) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); PrintWriter writer = new PrintWriter(out); asyncPrettyPrinter.printTo(writer, in); writer.flush(); return out.toString("UTF-8"); } } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/network/ResponseHandler.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.network; import java.io.IOException; /** * Custom hook to intercept read events delivered by * {@link NetworkEventReporter#interpretResponseStream}. */ public interface ResponseHandler { /** * Signal that data has been read from the response stream. * * @param numBytes Bytes read from the network stack's stream as established by * {@link NetworkEventReporter#interpretResponseStream}. */ void onRead(int numBytes); /** * Signal that data has been decoded (reversing the response's {@code Content-Encoding}) while * reading a raw stream. This method is only called when the stream is known to have * a supported encoding. Note that for HTTP, content encoding almost always is used for * some form of response compression. * * @param numBytes Bytes yielded after decoding bytes received from the network stack's * stream. */ void onReadDecoded(int numBytes); /** * Signals that EOF has been reached reading the response stream from the network * stack. */ void onEOF(); /** * Signals that an error occurred while reading the response stream. */ void onError(IOException e); } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/network/ResponseHandlingInputStream.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.network; import com.facebook.stetho.inspector.console.CLog; import com.facebook.stetho.inspector.helper.ChromePeerManager; import com.facebook.stetho.inspector.protocol.module.Console; import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.annotation.concurrent.GuardedBy; /** * {@link InputStream} that caches the data as the data is read, and writes them to the given * {@link OutputStream}. This also guarantees that we will attempt to reach EOF on the * {@link InputStream} passing all data to the {@link OutputStream}. * This is done to allow us to guarantee all responses are represented in the webkit inspector. */ // @VisibleForTest public final class ResponseHandlingInputStream extends FilterInputStream { public static final String TAG = "ResponseHandlingInputStream"; private static final int BUFFER_SIZE = 1024; private final String mRequestId; private final OutputStream mOutputStream; @Nullable private final CountingOutputStream mDecompressedCounter; private final ChromePeerManager mNetworkPeerManager; private final ResponseHandler mResponseHandler; /** * This stream will no longer be usable if {@link #close()} has been called on this stream. */ @GuardedBy("this") private boolean mClosed; @GuardedBy("this") private boolean mEofSeen; @Nullable @GuardedBy("this") private byte[] mSkipBuffer; private long mLastDecompressedCount = 0; /** * @param inputStream * @param requestId the requestId to use when we call the {@link NetworkEventReporter} * @param outputStream stream to write to. * @param decompressedCounter Optional decompressing counting output stream which * can be queried after each write to determine the number of decompressed bytes * yielded. Used to implement {@link ResponseHandler#onReadDecoded(int)}. * @param networkPeerManager A peer manager which is used to log internal errors to the * Inspector console. * @param responseHandler Special interface to intercept read events before they are sent * to peers via {@link NetworkEventReporter} methods. */ public ResponseHandlingInputStream( InputStream inputStream, String requestId, OutputStream outputStream, @Nullable CountingOutputStream decompressedCounter, ChromePeerManager networkPeerManager, ResponseHandler responseHandler) { super(inputStream); mRequestId = requestId; mOutputStream = outputStream; mDecompressedCounter = decompressedCounter; mNetworkPeerManager = networkPeerManager; mResponseHandler = responseHandler; mClosed = false; } private synchronized int checkEOF(int n) { if (n == -1) { closeOutputStreamQuietly(); mResponseHandler.onEOF(); mEofSeen = true; } return n; } @Override public int read() throws IOException { try { int result = checkEOF(in.read()); if (result != -1) { mResponseHandler.onRead(1); writeToOutputStream(result); } return result; } catch (IOException ex) { throw handleIOException(ex); } } @Override public int read(byte[] b) throws IOException { return this.read(b, 0, b.length); } @Override public int read(byte[] b, int off, int len) throws IOException { try { int result = checkEOF(in.read(b, off, len)); if (result != -1) { mResponseHandler.onRead(result); writeToOutputStream(b, off, result); } return result; } catch (IOException ex) { throw handleIOException(ex); } } @Override public synchronized long skip(long n) throws IOException { byte[] buffer = getSkipBufferLocked(); long total = 0; while (total < n) { long bytesDiff = n - total; int bytesToRead = (int) Math.min((long) buffer.length, bytesDiff); int result = this.read(buffer, 0, bytesToRead); if (result == -1) { break; } total += result; } return total; } @Nonnull private byte[] getSkipBufferLocked() { if (mSkipBuffer == null) { mSkipBuffer = new byte[BUFFER_SIZE]; } return mSkipBuffer; } @Override public boolean markSupported() { // this can be implemented, but isn't needed for TeedInputStream's behavior return false; } @Override public void mark(int readlimit) { // noop -- mark is not supported } @Override public void reset() throws IOException { throw new UnsupportedOperationException("Mark not supported"); } @Override public void close() throws IOException { try { long bytesRead = 0; if (!mEofSeen) { byte[] buffer = new byte[BUFFER_SIZE]; int count; while ((count = this.read(buffer)) != -1) { bytesRead += count; } } if (bytesRead > 0) { CLog.writeToConsole( mNetworkPeerManager, Console.MessageLevel.ERROR, Console.MessageSource.NETWORK, "There were " + String.valueOf(bytesRead) + " bytes that were not consumed while " + "processing request " + mRequestId); } } finally { super.close(); closeOutputStreamQuietly(); } } /** * Attempts to close all the output stream, and swallows any exceptions. */ private synchronized void closeOutputStreamQuietly() { if (!mClosed) { try { mOutputStream.close(); reportDecodedSizeIfApplicable(); } catch (IOException e) { CLog.writeToConsole( mNetworkPeerManager, Console.MessageLevel.ERROR, Console.MessageSource.NETWORK, "Could not close the output stream" + e); } finally { mClosed = true; } } } /** * Handles reporting an {@link IOException}. We do this so we can centralize the logic while still * maintaining the ability of the catch clause to throw. * @param ex * @return */ private IOException handleIOException(IOException ex) { mResponseHandler.onError(ex); return ex; } private void reportDecodedSizeIfApplicable() { if (mDecompressedCounter != null) { long currentCount = mDecompressedCounter.getCount(); int delta = (int)(currentCount - mLastDecompressedCount); mResponseHandler.onReadDecoded(delta); mLastDecompressedCount = currentCount; } } /** * Writes the byte to all the output streams. If we get an exception when writing to any * of the streams, we close all the streams, and then propagate the first exception that * occurred when writing. */ private synchronized void writeToOutputStream(int oneByte) { if (mClosed) { return; } try { mOutputStream.write(oneByte); reportDecodedSizeIfApplicable(); } catch (IOException e) { handleIOExceptionWritingToStream(e); } } /** * Same as {@link #writeToOutputStream(int)}, but we write a buffer instead. */ private synchronized void writeToOutputStream(byte[] b, int offset, int count) { if (mClosed) { return; } try { mOutputStream.write(b, offset, count); reportDecodedSizeIfApplicable(); } catch (IOException e) { handleIOExceptionWritingToStream(e); } } private void handleIOExceptionWritingToStream(IOException e) { CLog.writeToConsole( mNetworkPeerManager, Console.MessageLevel.ERROR, Console.MessageSource.NETWORK, "Could not write response body to the stream " + e); closeOutputStreamQuietly(); } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/network/SimpleBinaryInspectorWebSocketFrame.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.network; import java.io.UnsupportedEncodingException; public class SimpleBinaryInspectorWebSocketFrame implements NetworkEventReporter.InspectorWebSocketFrame { private final String mRequestId; private final byte[] mPayload; public SimpleBinaryInspectorWebSocketFrame(String requestId, byte[] payload) { mRequestId = requestId; mPayload = payload; } @Override public String requestId() { return mRequestId; } @Override public int opcode() { return OPCODE_BINARY; } @Override public boolean mask() { return false; } @Override public String payloadData() { try { // LOL, yes this is really how Chrome does it too... return new String(mPayload, "UTF-8"); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/network/SimpleTextInspectorWebSocketFrame.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.network; public class SimpleTextInspectorWebSocketFrame implements NetworkEventReporter.InspectorWebSocketFrame { private final String mRequestId; private final String mPayload; public SimpleTextInspectorWebSocketFrame(String requestId, String payload) { mRequestId = requestId; mPayload = payload; } @Override public String requestId() { return mRequestId; } @Override public int opcode() { return OPCODE_TEXT; } @Override public boolean mask() { return false; } @Override public String payloadData() { return mPayload; } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/protocol/ChromeDevtoolsDomain.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.protocol; /** * Marker interface that identifies implementations of subsystems in the WebKit Inspector protocol. */ public interface ChromeDevtoolsDomain { } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/protocol/ChromeDevtoolsMethod.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.protocol; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @Retention(RetentionPolicy.RUNTIME) public @interface ChromeDevtoolsMethod { } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/protocol/module/BaseDatabaseDriver.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.protocol.module; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteException; import java.util.List; /** * Extend {@link DatabaseDriver2} directly. This class is provided only as a common API compatible * base layer for the legacy {@link Database.DatabaseDriver}. */ public abstract class BaseDatabaseDriver { protected Context mContext; public BaseDatabaseDriver(Context context) { mContext = context; } public Context getContext() { return mContext; } /** * Access a stable list of objects that describe the databases made available by this driver. * The list of returned objects must not change on each invocation as this will cause * a memory leak when assigning unique identifiers for the objects to remote peers. */ public abstract List getDatabaseNames(); /** * Get or create a list of table names given a previously returned database descriptor instance * from {@link #getDatabaseNames()}. */ public abstract List getTableNames(DESC database); public abstract Database.ExecuteSQLResponse executeSQL( DESC database, String query, ExecuteResultHandler handler) throws SQLiteException; public interface ExecuteResultHandler { RESULT handleRawQuery() throws SQLiteException; RESULT handleSelect(Cursor result) throws SQLiteException; RESULT handleInsert(long insertedId) throws SQLiteException; RESULT handleUpdateDelete(int count) throws SQLiteException; } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/protocol/module/CSS.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.protocol.module; import android.annotation.SuppressLint; import com.facebook.stetho.common.ListUtil; import com.facebook.stetho.common.LogUtil; import com.facebook.stetho.common.StringUtil; import com.facebook.stetho.common.Util; import com.facebook.stetho.inspector.elements.ComputedStyleAccumulator; import com.facebook.stetho.inspector.elements.Document; import com.facebook.stetho.inspector.elements.Origin; import com.facebook.stetho.inspector.elements.StyleAccumulator; import com.facebook.stetho.inspector.elements.StyleRuleNameAccumulator; import com.facebook.stetho.inspector.helper.ChromePeerManager; import com.facebook.stetho.inspector.helper.PeersRegisteredListener; import com.facebook.stetho.inspector.jsonrpc.JsonRpcPeer; import com.facebook.stetho.inspector.jsonrpc.JsonRpcResult; import com.facebook.stetho.inspector.protocol.ChromeDevtoolsDomain; import com.facebook.stetho.inspector.protocol.ChromeDevtoolsMethod; import com.facebook.stetho.json.ObjectMapper; import com.facebook.stetho.json.annotation.JsonProperty; import org.json.JSONObject; import java.util.ArrayList; import java.util.Collections; import java.util.List; public class CSS implements ChromeDevtoolsDomain { private final ChromePeerManager mPeerManager; private final Document mDocument; private final ObjectMapper mObjectMapper; public CSS(Document document) { mDocument = Util.throwIfNull(document); mObjectMapper = new ObjectMapper(); mPeerManager = new ChromePeerManager(); mPeerManager.setListener(new PeerManagerListener()); } @ChromeDevtoolsMethod public void enable(JsonRpcPeer peer, JSONObject params) { } @ChromeDevtoolsMethod public void disable(JsonRpcPeer peer, JSONObject params) { } @ChromeDevtoolsMethod public JsonRpcResult getComputedStyleForNode(JsonRpcPeer peer, JSONObject params) { final GetComputedStyleForNodeRequest request = mObjectMapper.convertValue( params, GetComputedStyleForNodeRequest.class); final GetComputedStyleForNodeResult result = new GetComputedStyleForNodeResult(); result.computedStyle = new ArrayList<>(); mDocument.postAndWait(new Runnable() { @Override public void run() { Object element = mDocument.getElementForNodeId(request.nodeId); if (element == null) { LogUtil.e("Tried to get the style of an element that does not exist, using nodeid=" + request.nodeId); return; } mDocument.getElementComputedStyles( element, new ComputedStyleAccumulator() { @Override public void store(String name, String value) { final CSSComputedStyleProperty property = new CSSComputedStyleProperty(); property.name = name; property.value = value; result.computedStyle.add(property); } }); } }); return result; } @SuppressLint("DefaultLocale") @ChromeDevtoolsMethod public JsonRpcResult getMatchedStylesForNode(JsonRpcPeer peer, JSONObject params) { final GetMatchedStylesForNodeRequest request = mObjectMapper.convertValue( params, GetMatchedStylesForNodeRequest.class); final GetMatchedStylesForNodeResult result = new GetMatchedStylesForNodeResult(); result.matchedCSSRules = new ArrayList<>(); result.inherited = Collections.emptyList(); result.pseudoElements = Collections.emptyList(); mDocument.postAndWait(new Runnable() { @Override public void run() { final Object elementForNodeId = mDocument.getElementForNodeId(request.nodeId); if (elementForNodeId == null) { LogUtil.w("Failed to get style of an element that does not exist, nodeid=" + request.nodeId); return; } mDocument.getElementStyleRuleNames(elementForNodeId, new StyleRuleNameAccumulator() { @Override public void store(String ruleName, boolean editable) { final ArrayList properties = new ArrayList<>(); final RuleMatch match = new RuleMatch(); match.matchingSelectors = ListUtil.newImmutableList(0); final Selector selector = new Selector(); selector.value = ruleName; final CSSRule rule = new CSSRule(); rule.origin = Origin.REGULAR; rule.selectorList = new SelectorList(); rule.selectorList.selectors = ListUtil.newImmutableList(selector); rule.style = new CSSStyle(); rule.style.cssProperties = properties; rule.style.shorthandEntries = Collections.emptyList(); if (editable) { rule.style.styleSheetId = String.format( "%s.%s", Integer.toString(request.nodeId), selector.value); } mDocument.getElementStyles(elementForNodeId, ruleName, new StyleAccumulator() { @Override public void store(String name, String value, boolean isDefault) { final CSSProperty property = new CSSProperty(); property.name = name; property.value = value; properties.add(property); } }); match.rule = rule; result.matchedCSSRules.add(match); } }); } }); return result; } @ChromeDevtoolsMethod public SetPropertyTextResult setPropertyText(JsonRpcPeer peer, JSONObject params) { final SetPropertyTextRequest request = mObjectMapper.convertValue( params, SetPropertyTextRequest.class); final String[] parts = request.styleSheetId.split("\\.", 2); final int nodeId = Integer.parseInt(parts[0]); final String ruleName = parts[1]; final String value; final String key; if (request.text == null || !request.text.contains(":")) { key = null; value = null; } else { final String[] keyValue = request.text.split(":", 2); key = keyValue[0].trim(); value = StringUtil.removeAll(keyValue[1], ';').trim(); } final SetPropertyTextResult result = new SetPropertyTextResult(); result.style = new CSSStyle(); result.style.styleSheetId = request.styleSheetId; result.style.cssProperties = new ArrayList<>(); result.style.shorthandEntries = Collections.emptyList(); mDocument.postAndWait(new Runnable() { @Override public void run() { final Object elementForNodeId = mDocument.getElementForNodeId(nodeId); if (elementForNodeId == null) { LogUtil.w("Failed to get style of an element that does not exist, nodeid=" + nodeId); return; } if (key != null) { mDocument.setElementStyle(elementForNodeId, ruleName, key, value); } mDocument.getElementStyles(elementForNodeId, ruleName, new StyleAccumulator() { @Override public void store(String name, String value, boolean isDefault) { final CSSProperty property = new CSSProperty(); property.name = name; property.value = value; result.style.cssProperties.add(property); } }); } }); return result; } private final class PeerManagerListener extends PeersRegisteredListener { @Override protected synchronized void onFirstPeerRegistered() { mDocument.addRef(); } @Override protected synchronized void onLastPeerUnregistered() { mDocument.release(); } } private static class CSSComputedStyleProperty { @JsonProperty(required = true) public String name; @JsonProperty(required = true) public String value; } private static class RuleMatch { @JsonProperty public CSSRule rule; @JsonProperty public List matchingSelectors; } private static class SelectorList { @JsonProperty public List selectors; @JsonProperty public String text; } private static class SourceRange { @JsonProperty(required = true) public int startLine; @JsonProperty(required = true) public int startColumn; @JsonProperty(required = true) public int endLine; @JsonProperty(required = true) public int endColumn; } private static class Selector { @JsonProperty(required = true) public String value; @JsonProperty public SourceRange range; } private static class CSSRule { @JsonProperty public String styleSheetId; @JsonProperty(required = true) public SelectorList selectorList; @JsonProperty public Origin origin; @JsonProperty public CSSStyle style; } private static class CSSStyle { @JsonProperty public String styleSheetId; @JsonProperty(required = true) public List cssProperties; @JsonProperty public List shorthandEntries; @JsonProperty public String cssText; @JsonProperty public SourceRange range; } private static class ShorthandEntry { @JsonProperty(required = true) public String name; @JsonProperty(required = true) public String value; @JsonProperty public Boolean important; } private static class CSSProperty { @JsonProperty(required = true) public String name; @JsonProperty(required = true) public String value; @JsonProperty public Boolean important; @JsonProperty public Boolean implicit; @JsonProperty public String text; @JsonProperty public Boolean parsedOk; @JsonProperty public Boolean disabled; @JsonProperty public SourceRange range; } private static class PseudoIdMatches { @JsonProperty(required = true) public int pseudoId; @JsonProperty(required = true) public List matches; public PseudoIdMatches() { this.matches = new ArrayList<>(); } } private static class GetComputedStyleForNodeRequest { @JsonProperty(required = true) public int nodeId; } private static class InheritedStyleEntry { @JsonProperty(required = true) public CSSStyle inlineStyle; @JsonProperty(required = true) public List matchedCSSRules; } private static class GetComputedStyleForNodeResult implements JsonRpcResult { @JsonProperty(required = true) public List computedStyle; } private static class GetMatchedStylesForNodeRequest implements JsonRpcResult { @JsonProperty(required = true) public int nodeId; @JsonProperty public Boolean excludePseudo; @JsonProperty public Boolean excludeInherited; } private static class GetMatchedStylesForNodeResult implements JsonRpcResult { @JsonProperty public List matchedCSSRules; @JsonProperty public List pseudoElements; @JsonProperty public List inherited; } private static class SetPropertyTextRequest implements JsonRpcResult { @JsonProperty(required = true) public String styleSheetId; @JsonProperty(required = true) public String text; } private static class SetPropertyTextResult implements JsonRpcResult { @JsonProperty(required = true) public CSSStyle style; } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/protocol/module/Console.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.protocol.module; import android.annotation.SuppressLint; import com.facebook.stetho.inspector.console.ConsolePeerManager; import com.facebook.stetho.inspector.jsonrpc.JsonRpcPeer; import com.facebook.stetho.inspector.protocol.ChromeDevtoolsDomain; import com.facebook.stetho.inspector.protocol.ChromeDevtoolsMethod; import com.facebook.stetho.json.annotation.JsonProperty; import com.facebook.stetho.json.annotation.JsonValue; import org.json.JSONObject; public class Console implements ChromeDevtoolsDomain { public Console() { } @ChromeDevtoolsMethod public void enable(JsonRpcPeer peer, JSONObject params) { ConsolePeerManager.getOrCreateInstance().addPeer(peer); } @ChromeDevtoolsMethod public void disable(JsonRpcPeer peer, JSONObject params) { ConsolePeerManager.getOrCreateInstance().removePeer(peer); } @SuppressLint({ "UsingDefaultJsonDeserializer", "EmptyJsonPropertyUse" }) public static class MessageAddedRequest { @JsonProperty(required = true) public ConsoleMessage message; } @SuppressLint({ "UsingDefaultJsonDeserializer", "EmptyJsonPropertyUse" }) public static class ConsoleMessage { @JsonProperty(required = true) public MessageSource source; @JsonProperty(required = true) public MessageLevel level; @JsonProperty(required = true) public String text; } public enum MessageSource { XML("xml"), JAVASCRIPT("javascript"), NETWORK("network"), CONSOLE_API("console-api"), STORAGE("storage"), APPCACHE("appcache"), RENDERING("rendering"), CSS("css"), SECURITY("security"), OTHER("other"); private final String mProtocolValue; private MessageSource(String protocolValue) { mProtocolValue = protocolValue; } @JsonValue public String getProtocolValue() { return mProtocolValue; } } public enum MessageLevel { LOG("log"), WARNING("warning"), ERROR("error"), DEBUG("debug"); private final String mProtocolValue; private MessageLevel(String protocolValue) { mProtocolValue = protocolValue; } @JsonValue public String getProtocolValue() { return mProtocolValue; } } @SuppressLint({ "UsingDefaultJsonDeserializer", "EmptyJsonPropertyUse" }) public static class CallFrame { @JsonProperty(required = true) public String functionName; @JsonProperty(required = true) public String url; @JsonProperty(required = true) public int lineNumber; @JsonProperty(required = true) public int columnNumber; public CallFrame() { } public CallFrame(String functionName, String url, int lineNumber, int columnNumber) { this.functionName = functionName; this.url = url; this.lineNumber = lineNumber; this.columnNumber = columnNumber; } } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/protocol/module/DOM.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.protocol.module; import android.graphics.Color; import com.facebook.stetho.common.Accumulator; import com.facebook.stetho.common.ArrayListAccumulator; import com.facebook.stetho.common.LogUtil; import com.facebook.stetho.common.UncheckedCallable; import com.facebook.stetho.common.Util; import com.facebook.stetho.inspector.elements.DocumentView; import com.facebook.stetho.inspector.elements.Document; import com.facebook.stetho.inspector.elements.ElementInfo; import com.facebook.stetho.inspector.elements.NodeDescriptor; import com.facebook.stetho.inspector.elements.NodeType; import com.facebook.stetho.inspector.helper.ChromePeerManager; import com.facebook.stetho.inspector.helper.PeersRegisteredListener; import com.facebook.stetho.inspector.jsonrpc.JsonRpcException; import com.facebook.stetho.inspector.jsonrpc.JsonRpcPeer; import com.facebook.stetho.inspector.jsonrpc.JsonRpcResult; import com.facebook.stetho.inspector.jsonrpc.protocol.JsonRpcError; import com.facebook.stetho.inspector.protocol.ChromeDevtoolsDomain; import com.facebook.stetho.inspector.protocol.ChromeDevtoolsMethod; import com.facebook.stetho.json.ObjectMapper; import com.facebook.stetho.json.annotation.JsonProperty; import org.json.JSONObject; import javax.annotation.Nullable; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; public class DOM implements ChromeDevtoolsDomain { private final ObjectMapper mObjectMapper; private final Document mDocument; private final Map> mSearchResults; private final AtomicInteger mResultCounter; private final ChromePeerManager mPeerManager; private final DocumentUpdateListener mListener; private ChildNodeRemovedEvent mCachedChildNodeRemovedEvent; private ChildNodeInsertedEvent mCachedChildNodeInsertedEvent; public DOM(Document document) { mObjectMapper = new ObjectMapper(); mDocument = Util.throwIfNull(document); mSearchResults = Collections.synchronizedMap( new HashMap>()); mResultCounter = new AtomicInteger(0); mPeerManager = new ChromePeerManager(); mPeerManager.setListener(new PeerManagerListener()); mListener = new DocumentUpdateListener(); } @ChromeDevtoolsMethod public void enable(JsonRpcPeer peer, JSONObject params) { mPeerManager.addPeer(peer); } @ChromeDevtoolsMethod public void disable(JsonRpcPeer peer, JSONObject params) { mPeerManager.removePeer(peer); } @ChromeDevtoolsMethod public JsonRpcResult getDocument(JsonRpcPeer peer, JSONObject params) { final GetDocumentResponse result = new GetDocumentResponse(); result.root = mDocument.postAndWait(new UncheckedCallable() { @Override public Node call() { Object element = mDocument.getRootElement(); return createNodeForElement(element, mDocument.getDocumentView(), null); } }); return result; } @ChromeDevtoolsMethod public void highlightNode(JsonRpcPeer peer, JSONObject params) { final HighlightNodeRequest request = mObjectMapper.convertValue(params, HighlightNodeRequest.class); if (request.nodeId == null) { LogUtil.w("DOM.highlightNode was not given a nodeId; JS objectId is not supported"); return; } final RGBAColor contentColor = request.highlightConfig.contentColor; if (contentColor == null) { LogUtil.w("DOM.highlightNode was not given a color to highlight with"); return; } mDocument.postAndWait(new Runnable() { @Override public void run() { Object element = mDocument.getElementForNodeId(request.nodeId); if (element != null) { mDocument.highlightElement(element, contentColor.getColor()); } } }); } @ChromeDevtoolsMethod public void hideHighlight(JsonRpcPeer peer, JSONObject params) { mDocument.postAndWait(new Runnable() { @Override public void run() { mDocument.hideHighlight(); } }); } @ChromeDevtoolsMethod public ResolveNodeResponse resolveNode(JsonRpcPeer peer, JSONObject params) throws JsonRpcException { final ResolveNodeRequest request = mObjectMapper.convertValue(params, ResolveNodeRequest.class); final Object element = mDocument.postAndWait(new UncheckedCallable() { @Override public Object call() { return mDocument.getElementForNodeId(request.nodeId); } }); if (element == null) { throw new JsonRpcException( new JsonRpcError( JsonRpcError.ErrorCode.INVALID_PARAMS, "No known nodeId=" + request.nodeId, null /* data */)); } int mappedObjectId = Runtime.mapObject(peer, element); Runtime.RemoteObject remoteObject = new Runtime.RemoteObject(); remoteObject.type = Runtime.ObjectType.OBJECT; remoteObject.subtype = Runtime.ObjectSubType.NODE; remoteObject.className = element.getClass().getName(); remoteObject.value = null; // not a primitive remoteObject.description = null; // not sure what this does... remoteObject.objectId = String.valueOf(mappedObjectId); ResolveNodeResponse response = new ResolveNodeResponse(); response.object = remoteObject; return response; } @ChromeDevtoolsMethod public void setAttributesAsText(JsonRpcPeer peer, JSONObject params) { final SetAttributesAsTextRequest request = mObjectMapper.convertValue( params, SetAttributesAsTextRequest.class); mDocument.postAndWait(new Runnable() { @Override public void run() { Object element = mDocument.getElementForNodeId(request.nodeId); if (element != null) { mDocument.setAttributesAsText(element, request.text); } } }); } @ChromeDevtoolsMethod public void setInspectModeEnabled(JsonRpcPeer peer, JSONObject params) { final SetInspectModeEnabledRequest request = mObjectMapper.convertValue( params, SetInspectModeEnabledRequest.class); mDocument.postAndWait(new Runnable() { @Override public void run() { mDocument.setInspectModeEnabled(request.enabled); } }); } @ChromeDevtoolsMethod public PerformSearchResponse performSearch(JsonRpcPeer peer, final JSONObject params) { final PerformSearchRequest request = mObjectMapper.convertValue( params, PerformSearchRequest.class); final ArrayListAccumulator resultNodeIds = new ArrayListAccumulator<>(); mDocument.postAndWait(new Runnable() { @Override public void run() { mDocument.findMatchingElements(request.query, resultNodeIds); } }); // Each search action has a unique ID so that // it can be queried later. final String searchId = String.valueOf(mResultCounter.getAndIncrement()); mSearchResults.put(searchId, resultNodeIds); final PerformSearchResponse response = new PerformSearchResponse(); response.searchId = searchId; response.resultCount = resultNodeIds.size(); return response; } @ChromeDevtoolsMethod public GetSearchResultsResponse getSearchResults(JsonRpcPeer peer, JSONObject params) { final GetSearchResultsRequest request = mObjectMapper.convertValue( params, GetSearchResultsRequest.class); if (request.searchId == null) { LogUtil.w("searchId may not be null"); return null; } final List results = mSearchResults.get(request.searchId); if (results == null) { LogUtil.w("\"" + request.searchId + "\" is not a valid reference to a search result"); return null; } final List resultsRange = results.subList(request.fromIndex, request.toIndex); final GetSearchResultsResponse response = new GetSearchResultsResponse(); response.nodeIds = resultsRange; return response; } @ChromeDevtoolsMethod public void discardSearchResults(JsonRpcPeer peer, JSONObject params) { final DiscardSearchResultsRequest request = mObjectMapper.convertValue( params, DiscardSearchResultsRequest.class); if (request.searchId != null) { mSearchResults.remove(request.searchId); } } private Node createNodeForElement( Object element, DocumentView view, @Nullable Accumulator processedElements) { if (processedElements != null) { processedElements.store(element); } NodeDescriptor descriptor = mDocument.getNodeDescriptor(element); Node node = new DOM.Node(); node.nodeId = mDocument.getNodeIdForElement(element); node.nodeType = descriptor.getNodeType(element); node.nodeName = descriptor.getNodeName(element); node.localName = descriptor.getLocalName(element); node.nodeValue = descriptor.getNodeValue(element); Document.AttributeListAccumulator accumulator = new Document.AttributeListAccumulator(); descriptor.getAttributes(element, accumulator); // Attributes node.attributes = accumulator; // Children ElementInfo elementInfo = view.getElementInfo(element); List childrenNodes = (elementInfo.children.size() == 0) ? Collections.emptyList() : new ArrayList(elementInfo.children.size()); for (int i = 0, N = elementInfo.children.size(); i < N; ++i) { final Object childElement = elementInfo.children.get(i); Node childNode = createNodeForElement(childElement, view, processedElements); childrenNodes.add(childNode); } node.children = childrenNodes; node.childNodeCount = childrenNodes.size(); return node; } private ChildNodeInsertedEvent acquireChildNodeInsertedEvent() { ChildNodeInsertedEvent childNodeInsertedEvent = mCachedChildNodeInsertedEvent; if (childNodeInsertedEvent == null) { childNodeInsertedEvent = new ChildNodeInsertedEvent(); } mCachedChildNodeInsertedEvent = null; return childNodeInsertedEvent; } private void releaseChildNodeInsertedEvent(ChildNodeInsertedEvent childNodeInsertedEvent) { childNodeInsertedEvent.parentNodeId = -1; childNodeInsertedEvent.previousNodeId = -1; childNodeInsertedEvent.node = null; if (mCachedChildNodeInsertedEvent == null) { mCachedChildNodeInsertedEvent = childNodeInsertedEvent; } } private ChildNodeRemovedEvent acquireChildNodeRemovedEvent() { ChildNodeRemovedEvent childNodeRemovedEvent = mCachedChildNodeRemovedEvent; if (childNodeRemovedEvent == null) { childNodeRemovedEvent = new ChildNodeRemovedEvent(); } mCachedChildNodeRemovedEvent = null; return childNodeRemovedEvent; } private void releaseChildNodeRemovedEvent(ChildNodeRemovedEvent childNodeRemovedEvent) { childNodeRemovedEvent.parentNodeId = -1; childNodeRemovedEvent.nodeId = -1; if (mCachedChildNodeRemovedEvent == null) { mCachedChildNodeRemovedEvent = childNodeRemovedEvent; } } private final class DocumentUpdateListener implements Document.UpdateListener { public void onAttributeModified(Object element, String name, String value) { AttributeModifiedEvent message = new AttributeModifiedEvent(); message.nodeId = mDocument.getNodeIdForElement(element); message.name = name; message.value = value; mPeerManager.sendNotificationToPeers("DOM.attributeModified", message); } public void onAttributeRemoved(Object element, String name) { AttributeRemovedEvent message = new AttributeRemovedEvent(); message.nodeId = mDocument.getNodeIdForElement(element); message.name = name; mPeerManager.sendNotificationToPeers("DOM.attributeRemoved", message); } public void onInspectRequested(Object element) { Integer nodeId = mDocument.getNodeIdForElement(element); if (nodeId == null) { LogUtil.d( "DocumentProvider.Listener.onInspectRequested() " + "called for a non-mapped node: element=%s", element); } else { InspectNodeRequestedEvent message = new InspectNodeRequestedEvent(); message.nodeId = nodeId; mPeerManager.sendNotificationToPeers("DOM.inspectNodeRequested", message); } } public void onChildNodeRemoved( int parentNodeId, int nodeId) { ChildNodeRemovedEvent removedEvent = acquireChildNodeRemovedEvent(); removedEvent.parentNodeId = parentNodeId; removedEvent.nodeId = nodeId; mPeerManager.sendNotificationToPeers("DOM.childNodeRemoved", removedEvent); releaseChildNodeRemovedEvent(removedEvent); } public void onChildNodeInserted( DocumentView view, Object element, int parentNodeId, int previousNodeId, Accumulator insertedElements) { ChildNodeInsertedEvent insertedEvent = acquireChildNodeInsertedEvent(); insertedEvent.parentNodeId = parentNodeId; insertedEvent.previousNodeId = previousNodeId; insertedEvent.node = createNodeForElement(element, view, insertedElements); mPeerManager.sendNotificationToPeers("DOM.childNodeInserted", insertedEvent); releaseChildNodeInsertedEvent(insertedEvent); } } private final class PeerManagerListener extends PeersRegisteredListener { @Override protected synchronized void onFirstPeerRegistered() { mDocument.addRef(); mDocument.addUpdateListener(mListener); } @Override protected synchronized void onLastPeerUnregistered() { mSearchResults.clear(); mDocument.removeUpdateListener(mListener); mDocument.release(); } } private static class GetDocumentResponse implements JsonRpcResult { @JsonProperty(required = true) public Node root; } private static class Node implements JsonRpcResult { @JsonProperty(required = true) public int nodeId; @JsonProperty(required = true) public NodeType nodeType; @JsonProperty(required = true) public String nodeName; @JsonProperty(required = true) public String localName; @JsonProperty(required = true) public String nodeValue; @JsonProperty public Integer childNodeCount; @JsonProperty public List children; @JsonProperty public List attributes; } private static class AttributeModifiedEvent { @JsonProperty(required = true) public int nodeId; @JsonProperty(required = true) public String name; @JsonProperty(required = true) public String value; } private static class AttributeRemovedEvent { @JsonProperty(required = true) public int nodeId; @JsonProperty(required = true) public String name; } private static class ChildNodeInsertedEvent { @JsonProperty(required = true) public int parentNodeId; @JsonProperty(required = true) public int previousNodeId; @JsonProperty(required = true) public Node node; } private static class ChildNodeRemovedEvent { @JsonProperty(required = true) public int parentNodeId; @JsonProperty(required = true) public int nodeId; } private static class HighlightNodeRequest { @JsonProperty(required = true) public HighlightConfig highlightConfig; @JsonProperty public Integer nodeId; @JsonProperty public String objectId; } private static class HighlightConfig { @JsonProperty public RGBAColor contentColor; } private static class InspectNodeRequestedEvent { @JsonProperty public int nodeId; } private static class SetInspectModeEnabledRequest { @JsonProperty(required = true) public boolean enabled; @JsonProperty public Boolean inspectShadowDOM; @JsonProperty public HighlightConfig highlightConfig; } private static class RGBAColor { @JsonProperty(required = true) public int r; @JsonProperty(required = true) public int g; @JsonProperty(required = true) public int b; @JsonProperty public Double a; public int getColor() { byte alpha; if (this.a == null) { alpha = (byte)255; } else { long aLong = Math.round(this.a * 255.0); alpha = (aLong < 0) ? (byte)0 : (aLong >= 255) ? (byte)255 : (byte)aLong; } return Color.argb(alpha, this.r, this.g, this.b); } } private static class ResolveNodeRequest { @JsonProperty(required = true) public int nodeId; @JsonProperty public String objectGroup; } private static class SetAttributesAsTextRequest { @JsonProperty(required = true) public int nodeId; @JsonProperty(required = true) public String text; } private static class ResolveNodeResponse implements JsonRpcResult { @JsonProperty(required = true) public Runtime.RemoteObject object; } private static class PerformSearchRequest { @JsonProperty(required = true) public String query; @JsonProperty public Boolean includeUserAgentShadowDOM; } private static class PerformSearchResponse implements JsonRpcResult { @JsonProperty(required = true) public String searchId; @JsonProperty(required = true) public int resultCount; } private static class GetSearchResultsRequest { @JsonProperty(required = true) public String searchId; @JsonProperty(required = true) public int fromIndex; @JsonProperty(required = true) public int toIndex; } private static class GetSearchResultsResponse implements JsonRpcResult { @JsonProperty(required = true) public List nodeIds; } private static class DiscardSearchResultsRequest { @JsonProperty(required = true) public String searchId; } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/protocol/module/DOMStorage.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.protocol.module; import android.content.Context; import android.content.SharedPreferences; import com.facebook.stetho.inspector.console.CLog; import com.facebook.stetho.inspector.domstorage.DOMStoragePeerManager; import com.facebook.stetho.inspector.domstorage.SharedPreferencesHelper; import com.facebook.stetho.inspector.jsonrpc.JsonRpcException; import com.facebook.stetho.inspector.jsonrpc.JsonRpcPeer; import com.facebook.stetho.inspector.jsonrpc.JsonRpcResult; import com.facebook.stetho.inspector.protocol.ChromeDevtoolsDomain; import com.facebook.stetho.inspector.protocol.ChromeDevtoolsMethod; import com.facebook.stetho.json.ObjectMapper; import com.facebook.stetho.json.annotation.JsonProperty; import org.json.JSONException; import org.json.JSONObject; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; public class DOMStorage implements ChromeDevtoolsDomain { private final Context mContext; private final DOMStoragePeerManager mDOMStoragePeerManager; private final ObjectMapper mObjectMapper = new ObjectMapper(); public DOMStorage(Context context) { mContext = context; mDOMStoragePeerManager = new DOMStoragePeerManager(context); } @ChromeDevtoolsMethod public void enable(JsonRpcPeer peer, JSONObject params) { mDOMStoragePeerManager.addPeer(peer); } @ChromeDevtoolsMethod public void disable(JsonRpcPeer peer, JSONObject params) { mDOMStoragePeerManager.removePeer(peer); } @ChromeDevtoolsMethod public JsonRpcResult getDOMStorageItems(JsonRpcPeer peer, JSONObject params) throws JSONException { StorageId storage = mObjectMapper.convertValue( params.getJSONObject("storageId"), StorageId.class); ArrayList> entries = new ArrayList>(); String prefTag = storage.securityOrigin; if (storage.isLocalStorage) { SharedPreferences prefs = mContext.getSharedPreferences(prefTag, Context.MODE_PRIVATE); for (Map.Entry prefsEntry : SharedPreferencesHelper.getSharedPreferenceEntriesSorted(prefs)) { ArrayList entry = new ArrayList(2); entry.add(prefsEntry.getKey()); entry.add(SharedPreferencesHelper.valueToString(prefsEntry.getValue())); entries.add(entry); } } GetDOMStorageItemsResult result = new GetDOMStorageItemsResult(); result.entries = entries; return result; } @ChromeDevtoolsMethod public void setDOMStorageItem(JsonRpcPeer peer, JSONObject params) throws JSONException, JsonRpcException { StorageId storage = mObjectMapper.convertValue( params.getJSONObject("storageId"), StorageId.class); String key = params.getString("key"); String value = params.getString("value"); if (storage.isLocalStorage) { SharedPreferences prefs = mContext.getSharedPreferences( storage.securityOrigin, Context.MODE_PRIVATE); Object existingValue = prefs.getAll().get(key); try { if (existingValue == null) { throw new DOMStorageAssignmentException( "Unsupported: cannot add new key " + key + " due to lack of type inference"); } else { SharedPreferences.Editor editor = prefs.edit(); try { assignByType(editor, key, SharedPreferencesHelper.valueFromString(value, existingValue)); editor.apply(); } catch (IllegalArgumentException e) { throw new DOMStorageAssignmentException( String.format(Locale.US, "Type mismatch setting %s to %s (expected %s)", key, value, existingValue.getClass().getSimpleName())); } } } catch (DOMStorageAssignmentException e) { CLog.writeToConsole( mDOMStoragePeerManager, Console.MessageLevel.ERROR, Console.MessageSource.STORAGE, e.getMessage()); // Force the DevTools UI to refresh with the old value again (it assumes that the set // operation succeeded). Note that we should be able to do this by throwing // JsonRpcException but the UI doesn't respect setDOMStorageItem failure. if (prefs.contains(key)) { mDOMStoragePeerManager.signalItemUpdated( storage, key, value, SharedPreferencesHelper.valueToString(existingValue)); } else { mDOMStoragePeerManager.signalItemRemoved(storage, key); } } } } @ChromeDevtoolsMethod public void removeDOMStorageItem(JsonRpcPeer peer, JSONObject params) throws JSONException { StorageId storage = mObjectMapper.convertValue( params.getJSONObject("storageId"), StorageId.class); String key = params.getString("key"); if (storage.isLocalStorage) { SharedPreferences prefs = mContext.getSharedPreferences( storage.securityOrigin, Context.MODE_PRIVATE); prefs.edit().remove(key).apply(); } } private static void assignByType( SharedPreferences.Editor editor, String key, Object value) throws IllegalArgumentException { if (value instanceof Integer) { editor.putInt(key, (Integer)value); } else if (value instanceof Long) { editor.putLong(key, (Long)value); } else if (value instanceof Float) { editor.putFloat(key, (Float)value); } else if (value instanceof Boolean) { editor.putBoolean(key, (Boolean)value); } else if (value instanceof String) { editor.putString(key, (String)value); } else if (value instanceof Set) { editor.putStringSet(key, (Set)value); } else { throw new IllegalArgumentException("Unsupported type=" + value.getClass().getName()); } } public static class StorageId { @JsonProperty(required = true) public String securityOrigin; @JsonProperty(required = true) public boolean isLocalStorage; } private static class GetDOMStorageItemsResult implements JsonRpcResult { @JsonProperty(required = true) public List> entries; } public static class DomStorageItemsClearedParams { @JsonProperty(required = true) public StorageId storageId; } public static class DomStorageItemRemovedParams { @JsonProperty(required = true) public StorageId storageId; @JsonProperty(required = true) public String key; } public static class DomStorageItemAddedParams { @JsonProperty(required = true) public StorageId storageId; @JsonProperty(required = true) public String key; @JsonProperty(required = true) public String newValue; } public static class DomStorageItemUpdatedParams { @JsonProperty(required = true) public StorageId storageId; @JsonProperty(required = true) public String key; @JsonProperty(required = true) public String oldValue; @JsonProperty(required = true) public String newValue; } /** * Exception thrown internally when we fail to honor {@link #setDOMStorageItem}. */ private static class DOMStorageAssignmentException extends Exception { public DOMStorageAssignmentException(String message) { super(message); } } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/protocol/module/Database.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.protocol.module; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteException; import android.util.SparseArray; import com.facebook.stetho.common.LogUtil; import com.facebook.stetho.common.Util; import com.facebook.stetho.inspector.helper.ChromePeerManager; import com.facebook.stetho.inspector.helper.ObjectIdMapper; import com.facebook.stetho.inspector.helper.PeersRegisteredListener; import com.facebook.stetho.inspector.jsonrpc.JsonRpcException; import com.facebook.stetho.inspector.jsonrpc.JsonRpcPeer; import com.facebook.stetho.inspector.jsonrpc.JsonRpcResult; import com.facebook.stetho.inspector.jsonrpc.protocol.JsonRpcError; import com.facebook.stetho.inspector.protocol.ChromeDevtoolsDomain; import com.facebook.stetho.inspector.protocol.ChromeDevtoolsMethod; import com.facebook.stetho.json.ObjectMapper; import com.facebook.stetho.json.annotation.JsonProperty; import org.json.JSONObject; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import javax.annotation.concurrent.GuardedBy; import javax.annotation.concurrent.ThreadSafe; public class Database implements ChromeDevtoolsDomain { /** * The protocol doesn't offer an efficient means of pagination or anything like that so * we'll just cap the result list to some arbitrarily large number that I think folks will * actually need in practice. *

* Note that when this limit is exceeded, a dummy row will be introduced that indicates * truncation occurred. */ private static final int MAX_EXECUTE_RESULTS = 250; /** * Maximum length of a BLOB field before we stop trying to interpret it and just * return {@link #UNKNOWN_BLOB_LABEL} */ private static final int MAX_BLOB_LENGTH = 512; /** * Label to use when a BLOB column cannot be converted to a string. */ private static final String UNKNOWN_BLOB_LABEL = "{blob}"; private List mDatabaseDrivers; private final ChromePeerManager mChromePeerManager; private final DatabasePeerRegistrationListener mPeerListener; private final ObjectMapper mObjectMapper; /** * Constructs the object. */ public Database() { mDatabaseDrivers = new ArrayList<>(); mChromePeerManager = new ChromePeerManager(); mPeerListener = new DatabasePeerRegistrationListener(mDatabaseDrivers); mChromePeerManager.setListener(mPeerListener); mObjectMapper = new ObjectMapper(); } public void add(DatabaseDriver2 databaseDriver) { mDatabaseDrivers.add(databaseDriver); } @ChromeDevtoolsMethod public void enable(JsonRpcPeer peer, JSONObject params) { mChromePeerManager.addPeer(peer); } @ChromeDevtoolsMethod public void disable(JsonRpcPeer peer, JSONObject params) { mChromePeerManager.removePeer(peer); } @ChromeDevtoolsMethod public JsonRpcResult getDatabaseTableNames(JsonRpcPeer peer, JSONObject params) throws JsonRpcException { GetDatabaseTableNamesRequest request = mObjectMapper.convertValue(params, GetDatabaseTableNamesRequest.class); String databaseId = request.databaseId; DatabaseDescriptorHolder holder = mPeerListener.getDatabaseDescriptorHolder(databaseId); try { GetDatabaseTableNamesResponse response = new GetDatabaseTableNamesResponse(); response.tableNames = holder.driver.getTableNames(holder.descriptor); return response; } catch (SQLiteException e) { throw new JsonRpcException( new JsonRpcError( JsonRpcError.ErrorCode.INVALID_REQUEST, e.toString(), null /* data */)); } } @ChromeDevtoolsMethod public JsonRpcResult executeSQL(JsonRpcPeer peer, JSONObject params) { ExecuteSQLRequest request = mObjectMapper.convertValue(params, ExecuteSQLRequest.class); DatabaseDescriptorHolder holder = mPeerListener.getDatabaseDescriptorHolder(request.databaseId); try { return holder.driver.executeSQL( holder.descriptor, request.query, new DatabaseDriver.ExecuteResultHandler() { @Override public ExecuteSQLResponse handleRawQuery() throws SQLiteException { ExecuteSQLResponse response = new ExecuteSQLResponse(); // This is done because the inspector UI likes to delete rows if you give them no // name/value list response.columnNames = Collections.singletonList("success"); response.values = Collections.singletonList("true"); return response; } @Override public ExecuteSQLResponse handleSelect(Cursor result) throws SQLiteException { ExecuteSQLResponse response = new ExecuteSQLResponse(); response.columnNames = Arrays.asList(result.getColumnNames()); response.values = flattenRows(result, MAX_EXECUTE_RESULTS); return response; } @Override public ExecuteSQLResponse handleInsert(long insertedId) throws SQLiteException { ExecuteSQLResponse response = new ExecuteSQLResponse(); response.columnNames = Collections.singletonList("ID of last inserted row"); response.values = Collections.singletonList(String.valueOf(insertedId)); return response; } @Override public ExecuteSQLResponse handleUpdateDelete(int count) throws SQLiteException { ExecuteSQLResponse response = new ExecuteSQLResponse(); response.columnNames = Collections.singletonList("Modified rows"); response.values = Collections.singletonList(String.valueOf(count)); return response; } }); } catch (RuntimeException e) { LogUtil.e(e, "Exception executing: %s", request.query); Error error = new Error(); error.code = 0; error.message = e.getMessage(); ExecuteSQLResponse response = new ExecuteSQLResponse(); response.sqlError = error; return response; } } /** * Flatten all columns and all rows of a cursor to a single array. The array cannot be * interpreted meaningfully without the number of columns. * * @param cursor * @param limit Maximum number of rows to process. * @return List of Java primitives matching the value type of each column, converted to * strings. */ private static ArrayList flattenRows(Cursor cursor, int limit) { Util.throwIfNot(limit >= 0); ArrayList flatList = new ArrayList<>(); final int numColumns = cursor.getColumnCount(); for (int row = 0; row < limit && cursor.moveToNext(); row++) { for (int column = 0; column < numColumns; column++) { switch (cursor.getType(column)) { case Cursor.FIELD_TYPE_NULL: flatList.add(null); break; case Cursor.FIELD_TYPE_INTEGER: flatList.add(String.valueOf(cursor.getLong(column))); break; case Cursor.FIELD_TYPE_FLOAT: flatList.add(String.valueOf(cursor.getDouble(column))); break; case Cursor.FIELD_TYPE_BLOB: flatList.add(blobToString(cursor.getBlob(column))); break; case Cursor.FIELD_TYPE_STRING: default: flatList.add(cursor.getString(column)); break; } } } if (!cursor.isAfterLast()) { for (int column = 0; column < numColumns; column++) { flatList.add("{truncated}"); } } return flatList; } private static String blobToString(byte[] blob) { if (blob.length <= MAX_BLOB_LENGTH) { if (fastIsAscii(blob)) { try { return new String(blob, "US-ASCII"); } catch (UnsupportedEncodingException e) { // Fall through... } } } return UNKNOWN_BLOB_LABEL; } private static boolean fastIsAscii(byte[] blob) { for (byte b : blob) { if ((b & ~0x7f) != 0) { return false; } } return true; } @ThreadSafe private static class DatabasePeerRegistrationListener extends PeersRegisteredListener { private final List mDatabaseDrivers; @GuardedBy("this") private final SparseArray mDatabaseHolders = new SparseArray<>(); @GuardedBy("this") private final ObjectIdMapper mDatabaseIdMapper = new ObjectIdMapper(); private DatabasePeerRegistrationListener(List databaseDrivers) { mDatabaseDrivers = databaseDrivers; } public DatabaseDescriptorHolder getDatabaseDescriptorHolder(String databaseId) { return mDatabaseHolders.get(Integer.parseInt(databaseId)); } @Override protected synchronized void onFirstPeerRegistered() { for (DatabaseDriver2 driver : mDatabaseDrivers) { for (DatabaseDescriptor desc : driver.getDatabaseNames()) { Integer databaseId = mDatabaseIdMapper.getIdForObject(desc); if (databaseId == null) { databaseId = mDatabaseIdMapper.putObject(desc); mDatabaseHolders.put( databaseId, new DatabaseDescriptorHolder(driver, desc)); } } } } @Override protected synchronized void onLastPeerUnregistered() { mDatabaseIdMapper.clear(); mDatabaseHolders.clear(); } @Override protected synchronized void onPeerAdded(JsonRpcPeer peer) { for (int i = 0, N = mDatabaseHolders.size(); i < N; i++) { int id = mDatabaseHolders.keyAt(i); DatabaseDescriptorHolder holder = mDatabaseHolders.valueAt(i); Database.DatabaseObject databaseParams = new Database.DatabaseObject(); databaseParams.id = String.valueOf(id); databaseParams.name = holder.descriptor.name(); databaseParams.domain = holder.driver.getContext().getPackageName(); databaseParams.version = "N/A"; Database.AddDatabaseEvent eventParams = new Database.AddDatabaseEvent(); eventParams.database = databaseParams; peer.invokeMethod("Database.addDatabase", eventParams, null /* callback */); } } @Override protected synchronized void onPeerRemoved(JsonRpcPeer peer) { // Nothing to do on each peer removal... } } private static class DatabaseDescriptorHolder { public final DatabaseDriver2 driver; public final DatabaseDescriptor descriptor; public DatabaseDescriptorHolder(DatabaseDriver2 driver, DatabaseDescriptor descriptor) { this.driver = driver; this.descriptor = descriptor; } } private static class GetDatabaseTableNamesRequest { @JsonProperty(required = true) public String databaseId; } private static class GetDatabaseTableNamesResponse implements JsonRpcResult { @JsonProperty(required = true) public List tableNames; } public static class ExecuteSQLRequest { @JsonProperty(required = true) public String databaseId; @JsonProperty(required = true) public String query; } public static class ExecuteSQLResponse implements JsonRpcResult { @JsonProperty public List columnNames; @JsonProperty public List values; @JsonProperty public Error sqlError; } public static class AddDatabaseEvent { @JsonProperty(required = true) public DatabaseObject database; } public static class DatabaseObject { @JsonProperty(required = true) public String id; @JsonProperty(required = true) public String domain; @JsonProperty(required = true) public String name; @JsonProperty(required = true) public String version; } public static class Error { @JsonProperty(required = true) public String message; @JsonProperty(required = true) public int code; } /** * @deprecated Use {@link DatabaseDriver2} which allows for structured identifiers of database * objects (such as a file path instead of just a string name) which also serves as a * namespacing separation of multiple drivers. */ @Deprecated public static abstract class DatabaseDriver extends BaseDatabaseDriver { public DatabaseDriver(Context context) { super(context); } } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/protocol/module/DatabaseConstants.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.protocol.module; import android.os.Build; public interface DatabaseConstants { /** * Minimum API version required to use the {@link Database}. */ public static final int MIN_API_LEVEL = Build.VERSION_CODES.ICE_CREAM_SANDWICH; } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/protocol/module/DatabaseDescriptor.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.protocol.module; public interface DatabaseDescriptor { /** * The user visible name for this database. */ String name(); } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/protocol/module/DatabaseDriver2.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.protocol.module; import android.content.Context; /** * Replaces {@link Database.DatabaseDriver} to enforce that the generic type must * extend {@link DatabaseDescriptor}. */ public abstract class DatabaseDriver2 extends BaseDatabaseDriver { public DatabaseDriver2(Context context) { super(context); } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/protocol/module/Debugger.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.protocol.module; import com.facebook.stetho.inspector.jsonrpc.JsonRpcPeer; import com.facebook.stetho.inspector.jsonrpc.JsonRpcResult; import com.facebook.stetho.inspector.protocol.ChromeDevtoolsDomain; import com.facebook.stetho.inspector.protocol.ChromeDevtoolsMethod; import org.json.JSONObject; public class Debugger implements ChromeDevtoolsDomain { public Debugger() { } @ChromeDevtoolsMethod public void enable(JsonRpcPeer peer, JSONObject params) { } @ChromeDevtoolsMethod public void disable(JsonRpcPeer peer, JSONObject params) { } @ChromeDevtoolsMethod public JsonRpcResult canSetScriptSource(JsonRpcPeer peer, JSONObject params) { return new SimpleBooleanResult(false); } @ChromeDevtoolsMethod public void setPauseOnExceptions(JsonRpcPeer peer, JSONObject params) { } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/protocol/module/HeapProfiler.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.protocol.module; import java.util.Collections; import java.util.List; import com.facebook.stetho.inspector.jsonrpc.JsonRpcPeer; import com.facebook.stetho.inspector.jsonrpc.JsonRpcResult; import com.facebook.stetho.inspector.protocol.ChromeDevtoolsDomain; import com.facebook.stetho.inspector.protocol.ChromeDevtoolsMethod; import com.facebook.stetho.json.annotation.JsonProperty; import org.json.JSONObject; public class HeapProfiler implements ChromeDevtoolsDomain { public HeapProfiler() { } @ChromeDevtoolsMethod public JsonRpcResult getProfileHeaders(JsonRpcPeer peer, JSONObject params) { ProfileHeaderResponse response = new ProfileHeaderResponse(); response.headers = Collections.emptyList(); return response; } private static class ProfileHeaderResponse implements JsonRpcResult { @JsonProperty(required = true) public List headers; } private static class ProfileHeader { @JsonProperty(required = true) public String title; @JsonProperty(required = true) public int uid; } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/protocol/module/Inspector.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.protocol.module; import com.facebook.stetho.inspector.jsonrpc.JsonRpcPeer; import com.facebook.stetho.inspector.protocol.ChromeDevtoolsDomain; import com.facebook.stetho.inspector.protocol.ChromeDevtoolsMethod; import org.json.JSONObject; public class Inspector implements ChromeDevtoolsDomain { public Inspector() { } @ChromeDevtoolsMethod public void enable(JsonRpcPeer peer, JSONObject params) { } @ChromeDevtoolsMethod public void disable(JsonRpcPeer peer, JSONObject params) { } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/protocol/module/Network.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.protocol.module; import java.io.IOException; import java.util.List; import android.content.Context; import com.facebook.stetho.common.Util; import com.facebook.stetho.inspector.jsonrpc.JsonRpcException; import com.facebook.stetho.inspector.jsonrpc.JsonRpcPeer; import com.facebook.stetho.inspector.jsonrpc.JsonRpcResult; import com.facebook.stetho.inspector.jsonrpc.protocol.JsonRpcError; import com.facebook.stetho.inspector.network.AsyncPrettyPrinterInitializer; import com.facebook.stetho.inspector.network.NetworkPeerManager; import com.facebook.stetho.inspector.network.ResponseBodyData; import com.facebook.stetho.inspector.network.ResponseBodyFileManager; import com.facebook.stetho.inspector.protocol.ChromeDevtoolsDomain; import com.facebook.stetho.inspector.protocol.ChromeDevtoolsMethod; import com.facebook.stetho.json.annotation.JsonProperty; import com.facebook.stetho.json.annotation.JsonValue; import org.json.JSONException; import org.json.JSONObject; public class Network implements ChromeDevtoolsDomain { private final NetworkPeerManager mNetworkPeerManager; private final ResponseBodyFileManager mResponseBodyFileManager; public Network(Context context) { mNetworkPeerManager = NetworkPeerManager.getOrCreateInstance(context); mResponseBodyFileManager = mNetworkPeerManager.getResponseBodyFileManager(); } @ChromeDevtoolsMethod public void enable(JsonRpcPeer peer, JSONObject params) { mNetworkPeerManager.addPeer(peer); } @ChromeDevtoolsMethod public void disable(JsonRpcPeer peer, JSONObject params) { mNetworkPeerManager.removePeer(peer); } @ChromeDevtoolsMethod public void setUserAgentOverride(JsonRpcPeer peer, JSONObject params) { // Not implemented... } @ChromeDevtoolsMethod public JsonRpcResult getResponseBody(JsonRpcPeer peer, JSONObject params) throws JsonRpcException { try { String requestId = params.getString("requestId"); return readResponseBody(requestId); } catch (IOException e) { throw new JsonRpcException(new JsonRpcError(JsonRpcError.ErrorCode.INTERNAL_ERROR, e.toString(), null /* data */)); } catch (JSONException e) { throw new JsonRpcException(new JsonRpcError(JsonRpcError.ErrorCode.INTERNAL_ERROR, e.toString(), null /* data */)); } } private GetResponseBodyResponse readResponseBody(String requestId) throws IOException, JsonRpcException { GetResponseBodyResponse response = new GetResponseBodyResponse(); ResponseBodyData bodyData; try { bodyData = mResponseBodyFileManager.readFile(requestId); } catch (OutOfMemoryError e) { throw new JsonRpcException(new JsonRpcError(JsonRpcError.ErrorCode.INTERNAL_ERROR, e.toString(), null /* data */)); } response.body = bodyData.data; response.base64Encoded = bodyData.base64Encoded; return response; } /** * Method that allows callers to provide an {@link AsyncPrettyPrinterInitializer} that is * responsible for registering all * {@link com.facebook.stetho.inspector.network.AsyncPrettyPrinter}. * Note that AsyncPrettyPrinterInitializer cannot be null and can only be set once. * @param initializer */ public void setPrettyPrinterInitializer(AsyncPrettyPrinterInitializer initializer) { Util.throwIfNull(initializer); mNetworkPeerManager.setPrettyPrinterInitializer(initializer); } private static class GetResponseBodyResponse implements JsonRpcResult { @JsonProperty(required = true) public String body; @JsonProperty(required = true) public boolean base64Encoded; } public static class RequestWillBeSentParams { @JsonProperty(required = true) public String requestId; @JsonProperty(required = true) public String frameId; @JsonProperty(required = true) public String loaderId; @JsonProperty(required = true) public String documentURL; @JsonProperty(required = true) public Request request; @JsonProperty(required = true) public double timestamp; @JsonProperty(required = true) public Initiator initiator; @JsonProperty public Response redirectResponse; @JsonProperty public Page.ResourceType type; } public static class ResponseReceivedParams { @JsonProperty(required = true) public String requestId; @JsonProperty(required = true) public String frameId; @JsonProperty(required = true) public String loaderId; @JsonProperty(required = true) public double timestamp; @JsonProperty(required = true) public Page.ResourceType type; @JsonProperty(required = true) public Response response; } public static class LoadingFinishedParams { @JsonProperty(required = true) public String requestId; @JsonProperty(required = true) public double timestamp; } public static class LoadingFailedParams { @JsonProperty(required = true) public String requestId; @JsonProperty(required = true) public double timestamp; @JsonProperty(required = true) public String errorText; // Chrome introduced this undocumented new addition that, if not sent, will cause the row // to be removed from the UI and raise a JavaScript exception in the console. This is // clearly an upstream bug that needs to be fixed, though we can work around it by // providing this new undocumented field. @JsonProperty public Page.ResourceType type; } public static class DataReceivedParams { @JsonProperty(required = true) public String requestId; @JsonProperty(required = true) public double timestamp; @JsonProperty(required = true) public int dataLength; @JsonProperty(required = true) public int encodedDataLength; } public static class Request { @JsonProperty(required = true) public String url; @JsonProperty(required = true) public String method; @JsonProperty(required = true) public JSONObject headers; @JsonProperty public String postData; } public static class Initiator { @JsonProperty(required = true) public InitiatorType type; @JsonProperty public List stackTrace; } public enum InitiatorType { PARSER("parser"), SCRIPT("script"), OTHER("other"); private final String mProtocolValue; private InitiatorType(String protocolValue) { mProtocolValue = protocolValue; } @JsonValue public String getProtocolValue() { return mProtocolValue; } } public static class Response { @JsonProperty(required = true) public String url; @JsonProperty(required = true) public int status; @JsonProperty(required = true) public String statusText; @JsonProperty(required = true) public JSONObject headers; @JsonProperty public String headersText; @JsonProperty(required = true) public String mimeType; @JsonProperty public JSONObject requestHeaders; @JsonProperty public String requestHeadersTest; @JsonProperty(required = true) public boolean connectionReused; @JsonProperty(required = true) public int connectionId; @JsonProperty(required = true) public Boolean fromDiskCache; @JsonProperty public ResourceTiming timing; } public static class ResourceTiming { @JsonProperty(required = true) public double requestTime; @JsonProperty(required = true) public double proxyStart; @JsonProperty(required = true) public double proxyEnd; @JsonProperty(required = true) public double dnsStart; @JsonProperty(required = true) public double dnsEnd; @JsonProperty(required = true) public double connectionStart; @JsonProperty(required = true) public double connectionEnd; @JsonProperty(required = true) public double sslStart; @JsonProperty(required = true) public double sslEnd; @JsonProperty(required = true) public double sendStart; @JsonProperty(required = true) public double sendEnd; @JsonProperty(required = true) public double receivedHeadersEnd; } public static class WebSocketCreatedParams { @JsonProperty(required = true) public String requestId; @JsonProperty(required = true) public String url; } public static class WebSocketClosedParams { @JsonProperty(required = true) public String requestId; @JsonProperty(required = true) public double timestamp; } public static class WebSocketWillSendHandshakeRequestParams { @JsonProperty(required = true) public String requestId; @JsonProperty(required = true) public double timestamp; @JsonProperty(required = true) public double wallTime; @JsonProperty(required = true) public WebSocketRequest request; } public static class WebSocketRequest { @JsonProperty(required = true) public JSONObject headers; } public static class WebSocketHandshakeResponseReceivedParams { @JsonProperty(required = true) public String requestId; @JsonProperty(required = true) public double timestamp; @JsonProperty(required = true) public WebSocketResponse response; } public static class WebSocketResponse { @JsonProperty(required = true) public int status; @JsonProperty(required = true) public String statusText; @JsonProperty(required = true) public JSONObject headers; @JsonProperty public String headersText; @JsonProperty public JSONObject requestHeaders; @JsonProperty public String requestHeadersText; } public static class WebSocketFrameReceivedParams { @JsonProperty(required = true) public String requestId; @JsonProperty(required = true) public double timestamp; @JsonProperty(required = true) public WebSocketFrame response; } public static class WebSocketFrameSentParams { @JsonProperty(required = true) public String requestId; @JsonProperty(required = true) public double timestamp; @JsonProperty(required = true) public WebSocketFrame response; } public static class WebSocketFrame { @JsonProperty(required = true) public int opcode; @JsonProperty(required = true) public boolean mask; @JsonProperty(required = true) public String payloadData; } public static class WebSocketFrameErrorParams { @JsonProperty(required = true) public String requestId; @JsonProperty(required = true) public double timestamp; @JsonProperty(required = true) public String errorMessage; } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/protocol/module/Page.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.protocol.module; import android.content.Context; import com.facebook.stetho.common.ProcessUtil; import com.facebook.stetho.inspector.domstorage.SharedPreferencesHelper; import com.facebook.stetho.inspector.jsonrpc.JsonRpcPeer; import com.facebook.stetho.inspector.jsonrpc.JsonRpcResult; import com.facebook.stetho.inspector.protocol.ChromeDevtoolsDomain; import com.facebook.stetho.inspector.protocol.ChromeDevtoolsMethod; import com.facebook.stetho.inspector.screencast.ScreencastDispatcher; import com.facebook.stetho.json.ObjectMapper; import com.facebook.stetho.json.annotation.JsonProperty; import com.facebook.stetho.json.annotation.JsonValue; import org.json.JSONObject; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; import androidx.annotation.Nullable; public class Page implements ChromeDevtoolsDomain { public static final String BANNER = // Note: not using Android resources so we can maintain .jar distribution for now. "_____/\\\\\\\\\\\\\\\\\\\\\\_______________________________________________/\\\\\\_______________________\n" + " ___/\\\\\\/////////\\\\\\____________________________________________\\/\\\\\\_______________________\n" + " __\\//\\\\\\______\\///______/\\\\\\_________________________/\\\\\\______\\/\\\\\\_______________________\n" + " ___\\////\\\\\\__________/\\\\\\\\\\\\\\\\\\\\\\_____/\\\\\\\\\\\\\\\\___/\\\\\\\\\\\\\\\\\\\\\\_\\/\\\\\\_____________/\\\\\\\\\\____\n" + " ______\\////\\\\\\______\\////\\\\\\////____/\\\\\\/////\\\\\\_\\////\\\\\\////__\\/\\\\\\\\\\\\\\\\\\\\____/\\\\\\///\\\\\\__\n" + " _________\\////\\\\\\______\\/\\\\\\_______/\\\\\\\\\\\\\\\\\\\\\\_____\\/\\\\\\______\\/\\\\\\/////\\\\\\__/\\\\\\__\\//\\\\\\_\n" + " __/\\\\\\______\\//\\\\\\_____\\/\\\\\\_/\\\\__\\//\\\\///////______\\/\\\\\\_/\\\\__\\/\\\\\\___\\/\\\\\\_\\//\\\\\\__/\\\\\\__\n" + " _\\///\\\\\\\\\\\\\\\\\\\\\\/______\\//\\\\\\\\\\____\\//\\\\\\\\\\\\\\\\\\\\____\\//\\\\\\\\\\___\\/\\\\\\___\\/\\\\\\__\\///\\\\\\\\\\/___\n" + " ___\\///////////_________\\/////______\\//////////______\\/////____\\///____\\///_____\\/////_____\n" + " Welcome to Stetho"; private final Context mContext; private final String mMessage; private final ObjectMapper mObjectMapper = new ObjectMapper(); @Nullable private ScreencastDispatcher mScreencastDispatcher; public Page(Context context) { this(context, BANNER); } public Page(Context context, String message) { mContext = context; mMessage = message; } @ChromeDevtoolsMethod public void enable(JsonRpcPeer peer, JSONObject params) { notifyExecutionContexts(peer); sendWelcomeMessage(peer); } @ChromeDevtoolsMethod public void disable(JsonRpcPeer peer, JSONObject params) { } private void notifyExecutionContexts(JsonRpcPeer peer) { ExecutionContextDescription context = new ExecutionContextDescription(); context.frameId = "1"; context.id = 1; ExecutionContextCreatedParams params = new ExecutionContextCreatedParams(); params.context = context; peer.invokeMethod("Runtime.executionContextCreated", params, null /* callback */); } private void sendWelcomeMessage(JsonRpcPeer peer) { Console.ConsoleMessage message = new Console.ConsoleMessage(); message.source = Console.MessageSource.JAVASCRIPT; message.level = Console.MessageLevel.LOG; message.text = mMessage + "\n" + " Attached to " + ProcessUtil.getProcessName() + "\n"; Console.MessageAddedRequest messageAddedRequest = new Console.MessageAddedRequest(); messageAddedRequest.message = message; peer.invokeMethod("Console.messageAdded", messageAddedRequest, null /* callback */); } // Dog science... @ChromeDevtoolsMethod public JsonRpcResult getResourceTree(JsonRpcPeer peer, JSONObject params) { // The DOMStorage module expects one key/value store per "security origin" which has a 1:1 // relationship with resource tree frames. List prefsTags = SharedPreferencesHelper.getSharedPreferenceTags(mContext); Iterator prefsTagsIter = prefsTags.iterator(); FrameResourceTree tree = createSimpleFrameResourceTree( "1", null /* parentId */, "Stetho", prefsTagsIter.hasNext() ? prefsTagsIter.next() : ""); if (tree.childFrames == null) { tree.childFrames = new ArrayList(); } int nextChildFrameId = 1; while (prefsTagsIter.hasNext()) { String frameId = "1." + (nextChildFrameId++); String prefsTag = prefsTagsIter.next(); FrameResourceTree child = createSimpleFrameResourceTree( frameId, "1", "Child #" + frameId, prefsTag); tree.childFrames.add(child); } GetResourceTreeParams resultParams = new GetResourceTreeParams(); resultParams.frameTree = tree; return resultParams; } private static FrameResourceTree createSimpleFrameResourceTree( String id, String parentId, String name, String securityOrigin) { Frame frame = new Frame(); frame.id = id; frame.parentId = parentId; frame.loaderId = "1"; frame.name = name; frame.url = ""; frame.securityOrigin = securityOrigin; frame.mimeType = "text/plain"; FrameResourceTree tree = new FrameResourceTree(); tree.frame = frame; tree.resources = Collections.emptyList(); tree.childFrames = null; return tree; } @ChromeDevtoolsMethod public JsonRpcResult canScreencast(JsonRpcPeer peer, JSONObject params) { return new SimpleBooleanResult(true); } @ChromeDevtoolsMethod public JsonRpcResult hasTouchInputs(JsonRpcPeer peer, JSONObject params) { return new SimpleBooleanResult(false); } @ChromeDevtoolsMethod public void setDeviceMetricsOverride(JsonRpcPeer peer, JSONObject params) { } @ChromeDevtoolsMethod public void clearDeviceOrientationOverride(JsonRpcPeer peer, JSONObject params) { } @ChromeDevtoolsMethod public void startScreencast(final JsonRpcPeer peer, JSONObject params) { final StartScreencastRequest request = mObjectMapper.convertValue( params, StartScreencastRequest.class); if (mScreencastDispatcher == null) { mScreencastDispatcher = new ScreencastDispatcher(); mScreencastDispatcher.startScreencast(peer, request); } } @ChromeDevtoolsMethod public void stopScreencast(JsonRpcPeer peer, JSONObject params) { if (mScreencastDispatcher != null) { mScreencastDispatcher.stopScreencast(); mScreencastDispatcher = null; } } @ChromeDevtoolsMethod public void screencastFrameAck(JsonRpcPeer peer, JSONObject params) { // Nothing to do here, just need to make sure Chrome doesn't get an error that this method // isn't implemented } @ChromeDevtoolsMethod public void clearGeolocationOverride(JsonRpcPeer peer, JSONObject params) { } @ChromeDevtoolsMethod public void setTouchEmulationEnabled(JsonRpcPeer peer, JSONObject params) { } @ChromeDevtoolsMethod public void setEmulatedMedia(JsonRpcPeer peer, JSONObject params) { } @ChromeDevtoolsMethod public void setShowViewportSizeOnResize(JsonRpcPeer peer, JSONObject params) { } private static class GetResourceTreeParams implements JsonRpcResult { @JsonProperty(required = true) public FrameResourceTree frameTree; } private static class FrameResourceTree { @JsonProperty(required = true) public Frame frame; @JsonProperty public List childFrames; @JsonProperty(required = true) public List resources; } private static class Frame { @JsonProperty(required = true) public String id; @JsonProperty public String parentId; @JsonProperty(required = true) public String loaderId; @JsonProperty public String name; @JsonProperty(required = true) public String url; @JsonProperty(required = true) public String securityOrigin; @JsonProperty(required = true) public String mimeType; } private static class Resource { // Incomplete... } public enum ResourceType { DOCUMENT("Document"), STYLESHEET("Stylesheet"), IMAGE("Image"), FONT("Font"), SCRIPT("Script"), XHR("XHR"), WEBSOCKET("WebSocket"), OTHER("Other"); private final String mProtocolValue; private ResourceType(String protocolValue) { mProtocolValue = protocolValue; } @JsonValue public String getProtocolValue() { return mProtocolValue; } } private static class ExecutionContextCreatedParams { @JsonProperty(required = true) public ExecutionContextDescription context; } private static class ExecutionContextDescription { @JsonProperty(required = true) public String frameId; @JsonProperty(required = true) public int id; } public static class ScreencastFrameEvent { @JsonProperty(required = true) public String data; @JsonProperty(required = true) public ScreencastFrameEventMetadata metadata; } public static class ScreencastFrameEventMetadata { @JsonProperty(required = true) public int pageScaleFactor; @JsonProperty(required = true) public int offsetTop; @JsonProperty(required = true) public int deviceWidth; @JsonProperty(required = true) public int deviceHeight; @JsonProperty(required = true) public int scrollOffsetX; @JsonProperty(required = true) public int scrollOffsetY; } public static class StartScreencastRequest { @JsonProperty public String format; @JsonProperty public int quality; @JsonProperty public int maxWidth; @JsonProperty public int maxHeight; } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/protocol/module/Profiler.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.protocol.module; import java.util.Collections; import java.util.List; import com.facebook.stetho.inspector.jsonrpc.JsonRpcPeer; import com.facebook.stetho.inspector.jsonrpc.JsonRpcResult; import com.facebook.stetho.inspector.protocol.ChromeDevtoolsDomain; import com.facebook.stetho.inspector.protocol.ChromeDevtoolsMethod; import com.facebook.stetho.json.annotation.JsonProperty; import org.json.JSONObject; public class Profiler implements ChromeDevtoolsDomain { public Profiler() { } @ChromeDevtoolsMethod public void enable(JsonRpcPeer peer, JSONObject params) { } @ChromeDevtoolsMethod public void disable(JsonRpcPeer peer, JSONObject params) { } @ChromeDevtoolsMethod public void setSamplingInterval(JsonRpcPeer peer, JSONObject params) { } @ChromeDevtoolsMethod public JsonRpcResult getProfileHeaders(JsonRpcPeer peer, JSONObject params) { ProfileHeaderResponse response = new ProfileHeaderResponse(); response.headers = Collections.emptyList(); return response; } private static class ProfileHeaderResponse implements JsonRpcResult { @JsonProperty(required = true) public List headers; } private static class ProfileHeader { @JsonProperty(required = true) String typeId; @JsonProperty(required = true) String title; @JsonProperty(required = true) int uid; } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/protocol/module/Runtime.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.protocol.module; import android.content.Context; import com.facebook.stetho.Stetho; import com.facebook.stetho.common.LogUtil; import com.facebook.stetho.inspector.console.RuntimeRepl; import com.facebook.stetho.inspector.console.RuntimeReplFactory; import com.facebook.stetho.inspector.helper.ObjectIdMapper; import com.facebook.stetho.inspector.jsonrpc.DisconnectReceiver; import com.facebook.stetho.inspector.jsonrpc.JsonRpcException; import com.facebook.stetho.inspector.jsonrpc.JsonRpcPeer; import com.facebook.stetho.inspector.jsonrpc.JsonRpcResult; import com.facebook.stetho.inspector.jsonrpc.protocol.JsonRpcError; import com.facebook.stetho.inspector.protocol.ChromeDevtoolsDomain; import com.facebook.stetho.inspector.protocol.ChromeDevtoolsMethod; import com.facebook.stetho.inspector.runtime.RhinoDetectingRuntimeReplFactory; import com.facebook.stetho.json.ObjectMapper; import com.facebook.stetho.json.annotation.JsonProperty; import com.facebook.stetho.json.annotation.JsonValue; import org.json.JSONException; import org.json.JSONObject; import java.lang.reflect.Array; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import javax.annotation.Nonnull; import javax.annotation.Nullable; public class Runtime implements ChromeDevtoolsDomain { private final ObjectMapper mObjectMapper = new ObjectMapper(); private static final Map sSessions = Collections.synchronizedMap(new HashMap()); private final RuntimeReplFactory mReplFactory; /** * @deprecated Provided for ABI compatibility * * @see #Runtime(RuntimeReplFactory) * @see Stetho.DefaultInspectorModulesBuilder#runtimeRepl(RuntimeReplFactory) */ @Deprecated public Runtime() { this(new RuntimeReplFactory() { @Override public RuntimeRepl newInstance() { return new RuntimeRepl() { @Override public Object evaluate(String expression) throws Throwable { return "Not supported with legacy Runtime module"; } }; } }); } /** * @deprecated This was a transitionary API that was replaced by * {@link com.facebook.stetho.Stetho.DefaultInspectorModulesBuilder#runtimeRepl} */ public Runtime(Context context) { this(new RhinoDetectingRuntimeReplFactory(context)); } public Runtime(RuntimeReplFactory replFactory) { mReplFactory = replFactory; } public static int mapObject(JsonRpcPeer peer, Object object) { return getSession(peer).getObjects().putObject(object); } @Nonnull private static synchronized Session getSession(final JsonRpcPeer peer) { Session session = sSessions.get(peer); if (session == null) { session = new Session(); sSessions.put(peer, session); peer.registerDisconnectReceiver(new DisconnectReceiver() { @Override public void onDisconnect() { sSessions.remove(peer); } }); } return session; } /** * Removes objects from peer's session previously added by {@link #mapObject} */ public static void releaseObject(JsonRpcPeer peer, Integer id) throws JSONException { getSession(peer).getObjects().removeObjectById(id); } @ChromeDevtoolsMethod public void releaseObject(JsonRpcPeer peer, JSONObject params) throws JSONException { String objectId = params.getString("objectId"); getSession(peer).getObjects().removeObjectById(Integer.parseInt(objectId)); } @ChromeDevtoolsMethod public void releaseObjectGroup(JsonRpcPeer peer, JSONObject params) { LogUtil.w("Ignoring request to releaseObjectGroup: " + params); } @ChromeDevtoolsMethod public JsonRpcResult callFunctionOn(JsonRpcPeer peer, JSONObject params) throws JsonRpcException { CallFunctionOnRequest args = mObjectMapper.convertValue(params, CallFunctionOnRequest.class); Session session = getSession(peer); Object object = session.getObjectOrThrow(args.objectId); // The DevTools UI thinks it can run arbitrary JavaScript against us in order to figure out // the class structure of an object. That obviously won't fly, and there's no way to // translate without building a crude JavaScript parser so let's just go ahead and guess // what this function does by name. if (!args.functionDeclaration.startsWith("function protoList(")) { throw new JsonRpcException( new JsonRpcError( JsonRpcError.ErrorCode.INTERNAL_ERROR, "Expected protoList, got: " + args.functionDeclaration, null /* data */)); } // Since this is really a function call we have to create this fake object to hold the // "result" of the function. ObjectProtoContainer objectContainer = new ObjectProtoContainer(object); RemoteObject result = new RemoteObject(); result.type = ObjectType.OBJECT; result.subtype = ObjectSubType.NODE; result.className = object.getClass().getName(); result.description = getPropertyClassName(object); result.objectId = String.valueOf(session.getObjects().putObject(objectContainer)); CallFunctionOnResponse response = new CallFunctionOnResponse(); response.result = result; response.wasThrown = false; return response; } @ChromeDevtoolsMethod public JsonRpcResult evaluate(JsonRpcPeer peer, JSONObject params) { return getSession(peer).evaluate(mReplFactory, params); } @ChromeDevtoolsMethod public JsonRpcResult getProperties(JsonRpcPeer peer, JSONObject params) throws JsonRpcException { return getSession(peer).getProperties(params); } private static String getPropertyClassName(Object o) { String name = o.getClass().getSimpleName(); if (name == null || name.length() == 0) { // Looks better for anonymous classes. name = o.getClass().getName(); } return name; } private static class ObjectProtoContainer { public final Object object; public ObjectProtoContainer(Object object) { this.object = object; } } /** * Object representing a session with a single client. * *

Clients inherently leak object references because they can expand any object in the UI * at any time. Grouping references by client allows us to drop them when the client * disconnects. */ private static class Session { private final ObjectIdMapper mObjects = new ObjectIdMapper(); private final ObjectMapper mObjectMapper = new ObjectMapper(); @Nullable private RuntimeRepl mRepl; public ObjectIdMapper getObjects() { return mObjects; } public Object getObjectOrThrow(String objectId) throws JsonRpcException { Object object = getObjects().getObjectForId(Integer.parseInt(objectId)); if (object == null) { throw new JsonRpcException(new JsonRpcError( JsonRpcError.ErrorCode.INVALID_REQUEST, "No object found for " + objectId, null /* data */)); } return object; } public RemoteObject objectForRemote(Object value) { RemoteObject result = new RemoteObject(); if (value == null) { result.type = ObjectType.OBJECT; result.subtype = ObjectSubType.NULL; result.value = JSONObject.NULL; } else if (value instanceof Boolean) { result.type = ObjectType.BOOLEAN; result.value = value; } else if (value instanceof Number) { result.type = ObjectType.NUMBER; result.value = value; } else if (value instanceof Character) { // Unclear whether we should expose these as strings, numbers, or something else. result.type = ObjectType.NUMBER; result.value = Integer.valueOf(((Character)value).charValue()); } else if (value instanceof String) { result.type = ObjectType.STRING; result.value = String.valueOf(value); } else { result.type = ObjectType.OBJECT; result.className = "What??"; // I have no idea where this is used. result.objectId = String.valueOf(mObjects.putObject(value)); if (value.getClass().isArray()) { result.description = "array"; } else if (value instanceof List) { result.description = "List"; } else if (value instanceof Set) { result.description = "Set"; } else if (value instanceof Map) { result.description = "Map"; } else { result.description = getPropertyClassName(value); } } return result; } public EvaluateResponse evaluate(RuntimeReplFactory replFactory, JSONObject params) { EvaluateRequest request = mObjectMapper.convertValue(params, EvaluateRequest.class); try { if (!request.objectGroup.equals("console")) { return buildExceptionResponse("Not supported by FAB"); } RuntimeRepl repl = getRepl(replFactory); Object result = repl.evaluate(request.expression); return buildNormalResponse(result); } catch (Throwable t) { return buildExceptionResponse(t); } } @Nonnull private synchronized RuntimeRepl getRepl(RuntimeReplFactory replFactory) { if (mRepl == null) { mRepl = replFactory.newInstance(); } return mRepl; } private EvaluateResponse buildNormalResponse(Object retval) { EvaluateResponse response = new EvaluateResponse(); response.wasThrown = false; response.result = objectForRemote(retval); return response; } private EvaluateResponse buildExceptionResponse(Object retval) { EvaluateResponse response = new EvaluateResponse(); response.wasThrown = true; response.result = objectForRemote(retval); response.exceptionDetails = new ExceptionDetails(); response.exceptionDetails.text = retval.toString(); return response; } public GetPropertiesResponse getProperties(JSONObject params) throws JsonRpcException { GetPropertiesRequest request = mObjectMapper.convertValue(params, GetPropertiesRequest.class); if (!request.ownProperties) { GetPropertiesResponse response = new GetPropertiesResponse(); response.result = new ArrayList<>(); return response; } Object object = getObjectOrThrow(request.objectId); if (object.getClass().isArray()) { object = arrayToList(object); } if (object instanceof ObjectProtoContainer) { return getPropertiesForProtoContainer((ObjectProtoContainer) object); } else if (object instanceof List) { return getPropertiesForIterable((List) object, /* enumerate */ true); } else if (object instanceof Set) { return getPropertiesForIterable((Set) object, /* enumerate */ false); } else if (object instanceof Map) { return getPropertiesForMap(object); } else { return getPropertiesForObject(object); } } private List arrayToList(Object object) { Class type = object.getClass(); if (!type.isArray()) { throw new IllegalArgumentException("Argument must be an array. Was " + type); } Class component = type.getComponentType(); if (!component.isPrimitive()) { return Arrays.asList((Object[]) object); } // Loop manually for primitives. int length = Array.getLength(object); List ret = new ArrayList<>(length); for (int i = 0; i < length; i++) { ret.add(Array.get(object, i)); } return ret; } // Normally JavaScript will return the full class hierarchy as a list. That seems less // useful for Java since it's more natural (IMO) to see all available member variables in one // big list. private GetPropertiesResponse getPropertiesForProtoContainer(ObjectProtoContainer proto) { Object target = proto.object; RemoteObject protoRemote = new RemoteObject(); protoRemote.type = ObjectType.OBJECT; protoRemote.subtype = ObjectSubType.NODE; protoRemote.className = target.getClass().getName(); protoRemote.description = getPropertyClassName(target); protoRemote.objectId = String.valueOf(mObjects.putObject(target)); PropertyDescriptor descriptor = new PropertyDescriptor(); descriptor.name = "1"; descriptor.value = protoRemote; GetPropertiesResponse response = new GetPropertiesResponse(); response.result = new ArrayList<>(1); response.result.add(descriptor); return response; } private GetPropertiesResponse getPropertiesForIterable(Iterable object, boolean enumerate) { GetPropertiesResponse response = new GetPropertiesResponse(); List properties = new ArrayList<>(); int index = 0; for (Object value : object) { PropertyDescriptor property = new PropertyDescriptor(); property.name = enumerate ? String.valueOf(index++) : null; property.value = objectForRemote(value); properties.add(property); } response.result = properties; return response; } private GetPropertiesResponse getPropertiesForMap(Object object) { GetPropertiesResponse response = new GetPropertiesResponse(); List properties = new ArrayList<>(); for (Map.Entry entry : ((Map) object).entrySet()) { PropertyDescriptor property = new PropertyDescriptor(); property.name = String.valueOf(entry.getKey()); property.value = objectForRemote(entry.getValue()); properties.add(property); } response.result = properties; return response; } private GetPropertiesResponse getPropertiesForObject(Object object) { GetPropertiesResponse response = new GetPropertiesResponse(); List properties = new ArrayList<>(); for ( Class declaringClass = object.getClass(); declaringClass != null; declaringClass = declaringClass.getSuperclass() ) { // Reverse the list of fields while going up the superclass chain. // When we're done, we'll reverse the full list so that the superclasses // appear at the top, but within each class they properties are in declared order. List fields = new ArrayList(Arrays.asList(declaringClass.getDeclaredFields())); Collections.reverse(fields); String prefix = declaringClass == object.getClass() ? "" : declaringClass.getSimpleName() + "."; for (Field field : fields) { if (Modifier.isStatic(field.getModifiers())) { continue; } field.setAccessible(true); try { Object fieldValue = field.get(object); PropertyDescriptor property = new PropertyDescriptor(); property.name = prefix + field.getName(); property.value = objectForRemote(fieldValue); properties.add(property); } catch (IllegalAccessException e) { throw new RuntimeException(e); } } } Collections.reverse(properties); response.result = properties; return response; } } private static class CallFunctionOnRequest { @JsonProperty public String objectId; @JsonProperty public String functionDeclaration; @JsonProperty public List arguments; @JsonProperty(required = false) public Boolean doNotPauseOnExceptionsAndMuteConsole; @JsonProperty(required = false) public Boolean returnByValue; @JsonProperty(required = false) public Boolean generatePreview; } private static class CallFunctionOnResponse implements JsonRpcResult { @JsonProperty public RemoteObject result; @JsonProperty(required = false) public Boolean wasThrown; } private static class CallArgument { @JsonProperty(required = false) public Object value; @JsonProperty(required = false) public String objectId; @JsonProperty(required = false) public ObjectType type; } private static class GetPropertiesRequest implements JsonRpcResult { @JsonProperty(required = true) public boolean ownProperties; @JsonProperty(required = true) public String objectId; } private static class GetPropertiesResponse implements JsonRpcResult { @JsonProperty(required = true) public List result; } private static class EvaluateRequest implements JsonRpcResult { @JsonProperty(required = true) public String objectGroup; @JsonProperty(required = true) public String expression; } private static class EvaluateResponse implements JsonRpcResult { @JsonProperty(required = true) public RemoteObject result; @JsonProperty(required = true) public boolean wasThrown; @JsonProperty public ExceptionDetails exceptionDetails; } private static class ExceptionDetails { @JsonProperty(required = true) public String text; } public static class RemoteObject { @JsonProperty(required = true) public ObjectType type; @JsonProperty public ObjectSubType subtype; @JsonProperty public Object value; @JsonProperty public String className; @JsonProperty public String description; @JsonProperty public String objectId; } private static class PropertyDescriptor { @JsonProperty(required = true) public String name; @JsonProperty(required = true) public RemoteObject value; @JsonProperty(required = true) public final boolean isOwn = true; @JsonProperty(required = true) public final boolean configurable = false; @JsonProperty(required = true) public final boolean enumerable = true; @JsonProperty(required = true) public final boolean writable = false; } public static enum ObjectType { OBJECT("object"), FUNCTION("function"), UNDEFINED("undefined"), STRING("string"), NUMBER("number"), BOOLEAN("boolean"), SYMBOL("symbol"); private final String mProtocolValue; private ObjectType(String protocolValue) { mProtocolValue = protocolValue; } @JsonValue public String getProtocolValue() { return mProtocolValue; } } public static enum ObjectSubType { ARRAY("array"), NULL("null"), NODE("node"), REGEXP("regexp"), DATE("date"), MAP("map"), SET("set"), ITERATOR("iterator"), GENERATOR("generator"), ERROR("error"); private final String mProtocolValue; private ObjectSubType(String protocolValue) { mProtocolValue = protocolValue; } @JsonValue public String getProtocolValue() { return mProtocolValue; } } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/protocol/module/SimpleBooleanResult.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.protocol.module; import com.facebook.stetho.inspector.jsonrpc.JsonRpcResult; import com.facebook.stetho.json.annotation.JsonProperty; public class SimpleBooleanResult implements JsonRpcResult { @JsonProperty(required = true) public boolean result; public SimpleBooleanResult() { } public SimpleBooleanResult(boolean result) { this.result = result; } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/protocol/module/Worker.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.protocol.module; import com.facebook.stetho.inspector.jsonrpc.JsonRpcPeer; import com.facebook.stetho.inspector.jsonrpc.JsonRpcResult; import com.facebook.stetho.inspector.protocol.ChromeDevtoolsDomain; import com.facebook.stetho.inspector.protocol.ChromeDevtoolsMethod; import org.json.JSONObject; public class Worker implements ChromeDevtoolsDomain { public Worker() { } @ChromeDevtoolsMethod public JsonRpcResult canInspectWorkers(JsonRpcPeer peer, JSONObject params) { return new SimpleBooleanResult(true); } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/runtime/RhinoDetectingRuntimeReplFactory.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.runtime; import android.content.Context; import com.facebook.stetho.common.LogUtil; import com.facebook.stetho.inspector.console.RuntimeRepl; import com.facebook.stetho.inspector.console.RuntimeReplFactory; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import androidx.annotation.Nullable; /** * Attempts to locate stetho-js-rhino in the classpath and use it if available. Otherwise falls * back to a no-op version which informs folks that they can include stetho-js-rhino for more * advanced functionality. *

* Eventually we should develop a kind of service locator somehow to make this more discoverable * and generalized. For now with only one official implementation however it seems like overkill. */ public class RhinoDetectingRuntimeReplFactory implements RuntimeReplFactory { private final Context mContext; private boolean mSearchedForRhinoJs; private RuntimeReplFactory mRhinoReplFactory; private RuntimeException mRhinoJsUnexpectedError; public RhinoDetectingRuntimeReplFactory(Context context) { mContext = context; } @Override public RuntimeRepl newInstance() { if (!mSearchedForRhinoJs) { mSearchedForRhinoJs = true; try { mRhinoReplFactory = findRhinoReplFactory(mContext); } catch (RuntimeException e) { mRhinoJsUnexpectedError = e; } } if (mRhinoReplFactory != null) { return mRhinoReplFactory.newInstance(); } else { return new RuntimeRepl() { @Override public Object evaluate(String expression) throws Exception { if (mRhinoJsUnexpectedError != null) { return "stetho-js-rhino threw: " + mRhinoJsUnexpectedError.toString(); } else { return "Not supported without stetho-js-rhino dependency"; } } }; } } @Nullable private static RuntimeReplFactory findRhinoReplFactory(Context context) throws RuntimeException { try { Class jsRuntimeReplFactory = Class.forName("com.facebook.stetho.rhino.JsRuntimeReplFactoryBuilder"); Method defaultFactoryMethod = jsRuntimeReplFactory.getDeclaredMethod("defaultFactory", Context.class); return (RuntimeReplFactory) defaultFactoryMethod.invoke(null, context); } catch (ClassNotFoundException e) { LogUtil.i("Error finding stetho-js-rhino, cannot enable console evaluation!"); return null; } catch (NoSuchMethodException e) { throw new RuntimeException(e); } catch (InvocationTargetException e) { throw new RuntimeException(e); } catch (IllegalAccessException e) { throw new RuntimeException(e); } } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/inspector/screencast/ScreencastDispatcher.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.screencast; import android.app.Activity; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.RectF; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.util.Base64; import android.util.Base64OutputStream; import android.view.View; import com.facebook.stetho.common.LogUtil; import com.facebook.stetho.inspector.elements.android.ActivityTracker; import com.facebook.stetho.inspector.jsonrpc.JsonRpcPeer; import com.facebook.stetho.inspector.protocol.module.Page; import java.io.ByteArrayOutputStream; public final class ScreencastDispatcher { private static final long FRAME_DELAY = 200l; private final Handler mMainHandler = new Handler(Looper.getMainLooper()); private final BitmapFetchRunnable mBitmapFetchRunnable = new BitmapFetchRunnable(); private final ActivityTracker mActivityTracker = ActivityTracker.get(); private final EventDispatchRunnable mEventDispatchRunnable = new EventDispatchRunnable(); private final RectF mTempSrc = new RectF(); private final RectF mTempDst = new RectF(); private boolean mIsRunning; private Handler mBackgroundHandler; private JsonRpcPeer mPeer; private HandlerThread mHandlerThread; private Bitmap mBitmap; private Canvas mCanvas; private Page.StartScreencastRequest mRequest; private ByteArrayOutputStream mStream; private Page.ScreencastFrameEvent mEvent = new Page.ScreencastFrameEvent(); private Page.ScreencastFrameEventMetadata mMetadata = new Page.ScreencastFrameEventMetadata(); public ScreencastDispatcher() { } public void startScreencast(JsonRpcPeer peer, Page.StartScreencastRequest request) { LogUtil.d("Starting screencast"); mRequest = request; mHandlerThread = new HandlerThread("Screencast Thread"); mHandlerThread.start(); mPeer = peer; mIsRunning = true; mStream = new ByteArrayOutputStream(); mBackgroundHandler = new Handler(mHandlerThread.getLooper()); mMainHandler.postDelayed(mBitmapFetchRunnable, FRAME_DELAY); } public void stopScreencast() { LogUtil.d("Stopping screencast"); mBackgroundHandler.post(new CancellationRunnable()); } private class BitmapFetchRunnable implements Runnable { @Override public void run() { updateScreenBitmap(); mBackgroundHandler.post(mEventDispatchRunnable.withEndAction(this)); } private void updateScreenBitmap() { if (!mIsRunning) { return; } Activity activity = mActivityTracker.tryGetTopActivity(); if (activity == null) { return; } // This stuff needs to happen in the UI thread View rootView = activity.getWindow().getDecorView(); try { if (mBitmap == null) { int viewWidth = rootView.getWidth(); int viewHeight = rootView.getHeight(); float scale = Math.min((float) mRequest.maxWidth / (float) viewWidth, (float) mRequest.maxHeight / (float) viewHeight); int destWidth = (int) (viewWidth * scale); int destHeight = (int) (viewHeight * scale); mBitmap = Bitmap.createBitmap(destWidth, destHeight, Bitmap.Config.RGB_565); mCanvas = new Canvas(mBitmap); Matrix matrix = new Matrix(); mTempSrc.set(0, 0, viewWidth, viewHeight); mTempDst.set(0, 0, destWidth, destHeight); matrix.setRectToRect(mTempSrc, mTempDst, Matrix.ScaleToFit.CENTER); mCanvas.setMatrix(matrix); } rootView.draw(mCanvas); } catch (OutOfMemoryError e) { LogUtil.w("Out of memory trying to allocate screencast Bitmap."); } } } private class EventDispatchRunnable implements Runnable { private Runnable mEndAction; private EventDispatchRunnable withEndAction(Runnable endAction) { mEndAction = endAction; return this; } @Override public void run() { if (!mIsRunning || mBitmap == null) { return; } int width = mBitmap.getWidth(); int height = mBitmap.getHeight(); mStream.reset(); Base64OutputStream base64Stream = new Base64OutputStream(mStream, Base64.DEFAULT); // request format is either "jpeg" or "png" Bitmap.CompressFormat format = Bitmap.CompressFormat.valueOf(mRequest.format.toUpperCase()); mBitmap.compress(format, mRequest.quality, base64Stream); mEvent.data = mStream.toString(); mMetadata.pageScaleFactor = 1; mMetadata.deviceWidth = width; mMetadata.deviceHeight = height; mEvent.metadata = mMetadata; mPeer.invokeMethod("Page.screencastFrame", mEvent, null); mMainHandler.postDelayed(mEndAction, FRAME_DELAY); } } private class CancellationRunnable implements Runnable { @Override public void run() { mHandlerThread.interrupt(); mMainHandler.removeCallbacks(mBitmapFetchRunnable); mBackgroundHandler.removeCallbacks(mEventDispatchRunnable); mIsRunning = false; mHandlerThread = null; mBitmap = null; mCanvas = null; mStream = null; } } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/json/ObjectMapper.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.json; import javax.annotation.Nullable; import javax.annotation.concurrent.GuardedBy; import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import com.facebook.stetho.common.ExceptionUtil; import com.facebook.stetho.json.annotation.JsonProperty; import com.facebook.stetho.json.annotation.JsonValue; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; /** * This class is a lightweight version of Jackson's ObjectMapper. It is designed to have a minimal * subset of the functionality required for stetho. *

* It would be awesome if there were a lightweight library that supported converting between * arbitrary {@link Object} and {@link JSONObject} representations. *

* Admittedly the other approach would be to use an Annotation Processor to create static conversion * functions that discover something like a {@link JsonProperty} and create a function at compile * time however since this is just being used for a simple debug utility and Kit-Kat caches the * results of reflection this class is sufficient for stethos needs. */ public class ObjectMapper { @GuardedBy("mJsonValueMethodCache") private final Map, Method> mJsonValueMethodCache = new IdentityHashMap<>(); /** * Support mapping between arbitrary classes and {@link JSONObject}. * * It is possible for a {@link Throwable} to be propagated out of this class if there is an * {@link InvocationTargetException}. * * @param fromValue * @param toValueType * @param * @return * @throws IllegalArgumentException when there is an error converting. One of either * {@code fromValue.getClass()} or {@code toValueType} must be {@link JSONObject}. */ public T convertValue(Object fromValue, Class toValueType) throws IllegalArgumentException { if (fromValue == null) { return null; } if (toValueType != Object.class && toValueType.isAssignableFrom(fromValue.getClass())) { return (T) fromValue; } try { if (fromValue instanceof JSONObject) { return _convertFromJSONObject((JSONObject) fromValue, toValueType); } else if (toValueType == JSONObject.class) { return (T) _convertToJSONObject(fromValue); } else { throw new IllegalArgumentException( "Expecting either fromValue or toValueType to be a JSONObject"); } } catch (NoSuchMethodException e) { throw new IllegalArgumentException(e); } catch (IllegalAccessException e) { throw new IllegalArgumentException(e); } catch (InstantiationException e) { throw new IllegalArgumentException(e); } catch (JSONException e) { throw new IllegalArgumentException(e); } catch (InvocationTargetException e) { throw ExceptionUtil.propagate(e.getCause()); } } private T _convertFromJSONObject(JSONObject jsonObject, Class type) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, JSONException { Constructor constructor = type.getDeclaredConstructor((Class[]) null); constructor.setAccessible(true); T instance = constructor.newInstance(); Field[] fields = type.getFields(); for (int i = 0; i < fields.length; ++i) { Field field = fields[i]; if (Modifier.isStatic(field.getModifiers())) { continue; } Object value = jsonObject.opt(field.getName()); Object setValue = getValueForField(field, value); try { field.set(instance, setValue); } catch (IllegalArgumentException e) { throw new IllegalArgumentException( "Class: " + type.getSimpleName() + " " + "Field: " + field.getName() + " type " + (setValue != null ? setValue.getClass().getName() : "null"), e); } } return instance; } private Object getValueForField(Field field, Object value) throws JSONException { try { if (value != null) { if (value == JSONObject.NULL) { return null; } if (value.getClass() == field.getType()) { return value; } if (value instanceof JSONObject) { return convertValue(value, field.getType()); } else { if (field.getType().isEnum()) { return getEnumValue((String) value, field.getType().asSubclass(Enum.class)); } else if (value instanceof JSONArray) { return convertArrayToList(field, (JSONArray) value); } else if (value instanceof Number) { // Need to convert value to Number This happens because json treats 1 as an Integer even // if the field is supposed to be a Long Number numberValue = (Number) value; Class clazz = field.getType(); if (clazz == Integer.class || clazz == int.class) { return numberValue.intValue(); } else if (clazz == Long.class || clazz == long.class) { return numberValue.longValue(); } else if (clazz == Double.class || clazz == double.class) { return numberValue.doubleValue(); } else if (clazz == Float.class || clazz == float.class) { return numberValue.floatValue(); } else if (clazz == Byte.class || clazz == byte.class) { return numberValue.byteValue(); } else if (clazz == Short.class || clazz == short.class) { return numberValue.shortValue(); } else { throw new IllegalArgumentException("Not setup to handle class " + clazz.getName()); } } } } } catch (IllegalAccessException e) { throw new IllegalArgumentException("Unable to set value for field " + field.getName(), e); } return value; } private Enum getEnumValue(String value, Class clazz) { Method method = getJsonValueMethod(clazz); if (method != null) { return getEnumByMethod(value, clazz, method); } else { return Enum.valueOf(clazz, value); } } /** * In this case we know that there is an {@link Enum} decorated with {@link JsonValue}. This means * that we need to iterate through all of the values of the {@link Enum} returned by the given * {@link Method} to check the given value. * @param value * @param clazz * @param method * @return */ private Enum getEnumByMethod(String value, Class clazz, Method method) { Enum[] enumValues = clazz.getEnumConstants(); // Start at the front to ensure first always wins for (int i = 0; i < enumValues.length; ++i) { Enum enumValue = enumValues[i]; try { Object o = method.invoke(enumValue); if (o != null) { if (o.toString().equals(value)) { return enumValue; } } } catch (Exception ex) { throw new IllegalArgumentException(ex); } } throw new IllegalArgumentException("No enum constant " + clazz.getName() + "." + value); } private List convertArrayToList(Field field, JSONArray array) throws IllegalAccessException, JSONException { if (List.class.isAssignableFrom(field.getType())) { ParameterizedType parameterizedType = (ParameterizedType) field.getGenericType(); Type[] types = parameterizedType.getActualTypeArguments(); if (types.length != 1) { throw new IllegalArgumentException("Only able to handle a single type in a list " + field.getName()); } Class arrayClass = (Class)types[0]; List objectList = new ArrayList(); for (int i = 0; i < array.length(); ++i) { if (arrayClass.isEnum()) { objectList.add(getEnumValue(array.getString(i), arrayClass)); } else if (canDirectlySerializeClass(arrayClass)) { objectList.add(array.get(i)); } else { JSONObject jsonObject = array.getJSONObject(i); if (jsonObject == null) { objectList.add(null); } else { objectList.add(convertValue(jsonObject, arrayClass)); } } } return objectList; } else { throw new IllegalArgumentException("only know how to deserialize List on field " + field.getName()); } } private JSONObject _convertToJSONObject(Object fromValue) throws JSONException, InvocationTargetException, IllegalAccessException { JSONObject jsonObject = new JSONObject(); Field[] fields = fromValue.getClass().getFields(); for (int i = 0; i < fields.length; ++i) { Field field = fields[i]; if (Modifier.isStatic(field.getModifiers())) { continue; } JsonProperty property = field.getAnnotation(JsonProperty.class); if (property != null) { // AutoBox here ... Object value = field.get(fromValue); Class clazz = field.getType(); if (value != null) { clazz = value.getClass(); } String name = field.getName(); if (property.required() && value == null) { value = JSONObject.NULL; } else if (value == JSONObject.NULL) { // Leave it as null in this case. } else { value = getJsonValue(value, clazz, field); } jsonObject.put(name, value); } } return jsonObject; } private Object getJsonValue(Object value, Class clazz, Field field) throws InvocationTargetException, IllegalAccessException { if (value == null) { // Now technically we /could/ return JsonNode.NULL here but Chrome's webkit inspector croaks // if you pass a null "id" return null; } if (List.class.isAssignableFrom(clazz)) { return convertListToJsonArray(value); } // Finally check to see if there is a JsonValue present Method m = getJsonValueMethod(clazz); if (m != null) { return m.invoke(value); } if (!canDirectlySerializeClass(clazz)) { return convertValue(value, JSONObject.class); } // JSON has no support for NaN, Infinity or -Infinity, so we serialize // then as strings. Google Chrome's inspector will accept them just fine. if (clazz.equals(Double.class) || clazz.equals(Float.class)) { double doubleValue = ((Number) value).doubleValue(); if (Double.isNaN(doubleValue)) { return "NaN"; } else if (doubleValue == Double.POSITIVE_INFINITY) { return "Infinity"; } else if (doubleValue == Double.NEGATIVE_INFINITY) { return "-Infinity"; } } // hmm we should be able to directly serialize here... return value; } private JSONArray convertListToJsonArray(Object value) throws InvocationTargetException, IllegalAccessException { JSONArray array = new JSONArray(); List list = (List) value; for(Object obj : list) { // Send null, if this is an array of arrays we are screwed array.put(obj != null ? getJsonValue(obj, obj.getClass(), null /* field */) : null); } return array; } /** * * @param clazz * @return the first method annotated with {@link JsonValue} or null if one does not exist. */ @Nullable private Method getJsonValueMethod(Class clazz) { synchronized (mJsonValueMethodCache) { Method method = mJsonValueMethodCache.get(clazz); if (method == null && !mJsonValueMethodCache.containsKey(clazz)) { method = getJsonValueMethodImpl(clazz); mJsonValueMethodCache.put(clazz, method); } return method; } } @Nullable private static Method getJsonValueMethodImpl(Class clazz) { Method[] methods = clazz.getMethods(); for(int i = 0; i < methods.length; ++i) { Annotation jsonValue = methods[i].getAnnotation(JsonValue.class); if (jsonValue != null) { return methods[i]; } } return null; } private static boolean canDirectlySerializeClass(Class clazz) { return isWrapperOrPrimitiveType(clazz) || clazz.equals(String.class); } private static boolean isWrapperOrPrimitiveType(Class clazz) { return clazz.isPrimitive() || clazz.equals(Boolean.class) || clazz.equals(Integer.class) || clazz.equals(Character.class) || clazz.equals(Byte.class) || clazz.equals(Short.class) || clazz.equals(Double.class) || clazz.equals(Long.class) || clazz.equals(Float.class); } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/json/annotation/JsonProperty.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.json.annotation; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @Retention(RetentionPolicy.RUNTIME) public @interface JsonProperty { boolean required() default false; } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/json/annotation/JsonValue.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.json.annotation; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @Retention(RetentionPolicy.RUNTIME) public @interface JsonValue { } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/server/AddressNameHelper.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.server; import com.facebook.stetho.common.ProcessUtil; public class AddressNameHelper { private static final String PREFIX = "stetho_"; public static String createCustomAddress(String suffix) { final int userId = ProcessUtil.getUserId(); return PREFIX + ProcessUtil.getProcessName() + (userId == 0 ? "" : ("_" + userId)) + suffix; } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/server/CompositeInputStream.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.server; import com.facebook.stetho.common.LogUtil; import java.io.IOException; import java.io.InputStream; import javax.annotation.concurrent.NotThreadSafe; @NotThreadSafe public class CompositeInputStream extends InputStream { private final InputStream[] mStreams; private int mCurrentIndex; public CompositeInputStream(InputStream[] streams) { if (streams == null || streams.length < 2) { throw new IllegalArgumentException("Streams must be non-null and have more than 1 entry"); } mStreams = streams; mCurrentIndex = 0; } @Override public int available() throws IOException { return mStreams[mCurrentIndex].available(); } @Override public void close() throws IOException { closeAll(mCurrentIndex); } private void closeAll(int mostImportantIndex) throws IOException { IOException exceptionToThrow = null; for (int i = 0; i < mStreams.length; i++) { try { mStreams[i].close(); } catch (IOException e) { IOException previousException = exceptionToThrow; if (i == mostImportantIndex || exceptionToThrow == null) { exceptionToThrow = e; } if (previousException != null && previousException != exceptionToThrow) { LogUtil.w(previousException, "Suppressing exception"); } } } } @Override public void mark(int readlimit) { throw new UnsupportedOperationException(); } @Override public boolean markSupported() { return false; } @Override public void reset() throws IOException { throw new UnsupportedOperationException(); } @Override public int read(byte[] buffer) throws IOException { return read(buffer, 0, buffer.length); } @Override public int read(byte[] buffer, int byteOffset, int byteCount) throws IOException { int n; while ((n = mStreams[mCurrentIndex].read(buffer, byteOffset, byteCount)) == -1) { if (!tryMoveToNextStream()) { break; } } return n; } @Override public int read() throws IOException { int b; while ((b = mStreams[mCurrentIndex].read()) == -1) { if (!tryMoveToNextStream()) { break; } } return b; } private boolean tryMoveToNextStream() { if (mCurrentIndex + 1 < mStreams.length) { mCurrentIndex++; return true; } return false; } @Override public long skip(long byteCount) throws IOException { byte[] buf = new byte[(int)byteCount]; int n = read(buf); return n >= 0 ? n : -1; } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/server/LazySocketHandler.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.server; import android.net.LocalSocket; import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.io.IOException; /** * Optimization designed to allow us to lazily construct/configure the true Stetho server * only after the first caller connects. This gives us much more wiggle room to have performance * impact in the set up path that only applies when Stetho is _used_, not simply enabled. */ public class LazySocketHandler implements SocketHandler { private final SocketHandlerFactory mSocketHandlerFactory; @Nullable private SocketHandler mSocketHandler; public LazySocketHandler(SocketHandlerFactory socketHandlerFactory) { mSocketHandlerFactory = socketHandlerFactory; } @Override public void onAccepted(LocalSocket socket) throws IOException { getSocketHandler().onAccepted(socket); } @Nonnull private synchronized SocketHandler getSocketHandler() { if (mSocketHandler == null) { mSocketHandler = mSocketHandlerFactory.create(); } return mSocketHandler; } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/server/LeakyBufferedInputStream.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.server; import javax.annotation.concurrent.ThreadSafe; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; @ThreadSafe public class LeakyBufferedInputStream extends BufferedInputStream { private boolean mLeaked; private boolean mMarked; public LeakyBufferedInputStream(InputStream in, int bufSize) { super(in, bufSize); } @Override public synchronized void mark(int readlimit) { throwIfLeaked(); mMarked = true; super.mark(readlimit); } @Override public synchronized void reset() throws IOException { throwIfLeaked(); mMarked = false; super.reset(); } @Override public boolean markSupported() { return true; } public synchronized InputStream leakBufferAndStream() { throwIfLeaked(); throwIfMarked(); mLeaked = true; return new CompositeInputStream( new InputStream[] { new ByteArrayInputStream(clearBufferLocked()), in }); } private byte[] clearBufferLocked() { byte[] leaked = new byte[count - pos]; System.arraycopy(buf, pos, leaked, 0, leaked.length); pos = 0; count = 0; return leaked; } private void throwIfLeaked() { if (mLeaked) { throw new IllegalStateException(); } } private void throwIfMarked() { if (mMarked) { throw new IllegalStateException(); } } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/server/LocalSocketServer.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.server; import android.net.LocalServerSocket; import android.net.LocalSocket; import android.util.Log; import com.facebook.stetho.common.LogUtil; import com.facebook.stetho.common.Util; import javax.annotation.Nonnull; import java.io.IOException; import java.io.InterruptedIOException; import java.net.BindException; import java.net.SocketException; import java.util.concurrent.atomic.AtomicInteger; public class LocalSocketServer { private static final String WORKER_THREAD_NAME_PREFIX = "StethoWorker"; private static final int MAX_BIND_RETRIES = 2; private static final int TIME_BETWEEN_BIND_RETRIES_MS = 1000; private final String mFriendlyName; private final String mAddress; private final SocketHandler mSocketHandler; private final AtomicInteger mThreadId = new AtomicInteger(); private Thread mListenerThread; private boolean mStopped; private LocalServerSocket mServerSocket; /** * @param friendlyName identifier to help debug this server, used for naming threads and such. * @param address the local socket address to listen on. * @param socketHandler functional handler once a socket is accepted. */ public LocalSocketServer( String friendlyName, String address, SocketHandler socketHandler) { mFriendlyName = Util.throwIfNull(friendlyName); mAddress = Util.throwIfNull(address); mSocketHandler = socketHandler; } public String getName() { return mFriendlyName; } /** * Binds to the address and listens for connections. *

* If successful, this thread blocks forever or until {@link #stop} is called, whichever * happens first. * * @throws IOException Thrown on failure to bind the socket. */ public void run() throws IOException { synchronized (this) { if (mStopped) { return; } mListenerThread = Thread.currentThread(); } listenOnAddress(mAddress); } private void listenOnAddress(String address) throws IOException { mServerSocket = bindToSocket(address); LogUtil.i("Listening on @" + address); while (!Thread.interrupted()) { try { // Use previously accepted socket the first time around, otherwise wait to // accept another. LocalSocket socket = mServerSocket.accept(); // Start worker thread Thread t = new WorkerThread(socket, mSocketHandler); t.setName( WORKER_THREAD_NAME_PREFIX + "-" + mFriendlyName + "-" + mThreadId.incrementAndGet()); t.setDaemon(true); t.start(); } catch (SocketException se) { // ignore exception if interrupting the thread if (Thread.interrupted()) { break; } LogUtil.w(se, "I/O error"); } catch (InterruptedIOException ex) { break; } catch (IOException e) { LogUtil.w(e, "I/O error initialising connection thread"); break; } } LogUtil.i("Server shutdown on @" + address); } /** * Stops the listener thread and unbinds the address. */ public void stop() { synchronized (this) { mStopped = true; if (mListenerThread == null) { return; } } mListenerThread.interrupt(); try { if (mServerSocket != null) { mServerSocket.close(); } } catch (IOException e) { // Don't care... } } @Nonnull private static LocalServerSocket bindToSocket(String address) throws IOException { int retries = MAX_BIND_RETRIES; IOException firstException = null; do { try { if (LogUtil.isLoggable(Log.DEBUG)) { LogUtil.d("Trying to bind to @" + address); } return new LocalServerSocket(address); } catch (BindException be) { LogUtil.w(be, "Binding error, sleep " + TIME_BETWEEN_BIND_RETRIES_MS + " ms..."); if (firstException == null) { firstException = be; } Util.sleepUninterruptibly(TIME_BETWEEN_BIND_RETRIES_MS); } } while (retries-- > 0); throw firstException; } private static class WorkerThread extends Thread { private final LocalSocket mSocket; private final SocketHandler mSocketHandler; public WorkerThread(LocalSocket socket, SocketHandler socketHandler) { mSocket = socket; mSocketHandler = socketHandler; } @Override public void run() { try { mSocketHandler.onAccepted(mSocket); } catch (IOException ex) { LogUtil.w("I/O error: %s", ex); } finally { try { mSocket.close(); } catch (IOException ignore) { } } } } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/server/PeerAuthorizationException.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.server; public class PeerAuthorizationException extends Exception { public PeerAuthorizationException(String message) { super(message); } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/server/ProtocolDetectingSocketHandler.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.server; import android.content.Context; import android.net.LocalSocket; import javax.annotation.concurrent.NotThreadSafe; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Arrays; /** * Socket handler which is designed to detect a difference in protocol signatures very early on * in the connection to figure out which real handler to route to. This is used for performance * and backwards compatibility reasons to maintain Stetho having just one actual socket * connection despite dumpapp and DevTools now diverging in protocol. *

* Note this trick is only possible if the protocol requires that the client initiate the * conversation. Otherwise, the server would be expected to say something before we know what * protocol the client is speaking. */ public class ProtocolDetectingSocketHandler extends SecureSocketHandler { private static final int SENSING_BUFFER_SIZE = 256; private final ArrayList mHandlers = new ArrayList<>(2); public ProtocolDetectingSocketHandler(Context context) { super(context); } public void addHandler(MagicMatcher magicMatcher, SocketLikeHandler handler) { mHandlers.add(new HandlerInfo(magicMatcher, handler)); } @Override protected void onSecured(LocalSocket socket) throws IOException { LeakyBufferedInputStream leakyIn = new LeakyBufferedInputStream( socket.getInputStream(), SENSING_BUFFER_SIZE); if (mHandlers.isEmpty()) { throw new IllegalStateException("No handlers added"); } for (int i = 0, N = mHandlers.size(); i < N; i++) { HandlerInfo handlerInfo = mHandlers.get(i); leakyIn.mark(SENSING_BUFFER_SIZE); boolean matches = handlerInfo.magicMatcher.matches(leakyIn); leakyIn.reset(); if (matches) { SocketLike socketLike = new SocketLike(socket, leakyIn); handlerInfo.handler.onAccepted(socketLike); return; } } throw new IOException("No matching handler, firstByte=" + leakyIn.read()); } public interface MagicMatcher { boolean matches(InputStream in) throws IOException; } public static class ExactMagicMatcher implements MagicMatcher { private final byte[] mMagic; public ExactMagicMatcher(byte[] magic) { mMagic = magic; } @Override public boolean matches(InputStream in) throws IOException { byte[] buf = new byte[mMagic.length]; int n = in.read(buf); return n == buf.length && Arrays.equals(buf, mMagic); } } public static class AlwaysMatchMatcher implements MagicMatcher { @Override public boolean matches(InputStream in) throws IOException { return true; } } private static class HandlerInfo { public final MagicMatcher magicMatcher; public final SocketLikeHandler handler; private HandlerInfo(MagicMatcher magicMatcher, SocketLikeHandler handler) { this.magicMatcher = magicMatcher; this.handler = handler; } } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/server/SecureSocketHandler.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.server; import android.Manifest; import android.content.Context; import android.content.pm.PackageManager; import android.net.Credentials; import android.net.LocalSocket; import android.util.Log; import com.facebook.stetho.common.LogUtil; import java.io.IOException; public abstract class SecureSocketHandler implements SocketHandler { private final Context mContext; public SecureSocketHandler(Context context) { mContext = context; } @Override public final void onAccepted(LocalSocket socket) throws IOException { try { enforcePermission(mContext, socket); onSecured(socket); } catch (PeerAuthorizationException e) { LogUtil.e("Unauthorized request: " + e.getMessage()); } } protected abstract void onSecured(LocalSocket socket) throws IOException; private static void enforcePermission(Context context, LocalSocket peer) throws IOException, PeerAuthorizationException { Credentials credentials = peer.getPeerCredentials(); int uid = credentials.getUid(); int pid = credentials.getPid(); if (LogUtil.isLoggable(Log.VERBOSE)) { LogUtil.v("Got request from uid=%d, pid=%d", uid, pid); } String requiredPermission = Manifest.permission.DUMP; int checkResult = context.checkPermission(requiredPermission, pid, uid); if (checkResult != PackageManager.PERMISSION_GRANTED) { throw new PeerAuthorizationException( "Peer pid=" + pid + ", uid=" + uid + " does not have " + requiredPermission); } } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/server/ServerManager.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.server; import com.facebook.stetho.common.LogUtil; import java.io.IOException; import java.util.ArrayList; public class ServerManager { private static final String THREAD_PREFIX = "StethoListener"; private final LocalSocketServer mServer; private volatile boolean mStarted; public ServerManager(LocalSocketServer server) { mServer = server; } public void start() { if (mStarted) { throw new IllegalStateException("Already started"); } mStarted = true; startServer(mServer); } private void startServer(final LocalSocketServer server) { Thread listener = new Thread(THREAD_PREFIX + "-" + server.getName()) { @Override public void run() { try { server.run(); } catch (IOException e) { LogUtil.e(e, "Could not start Stetho server: %s", server.getName()); } } }; listener.start(); } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/server/SocketHandler.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.server; import android.net.LocalSocket; import java.io.IOException; /** * @see SecureSocketHandler */ public interface SocketHandler { /** * Server socket has been accepted and a dedicated thread has been allocated to process this * callback. Returning from this method or throwing an exception will attempt an orderly * shutdown of the socket, however it will not be treated as an error if returning normally. */ void onAccepted(LocalSocket socket) throws IOException; } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/server/SocketHandlerFactory.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.server; /** @see LazySocketHandler */ public interface SocketHandlerFactory { SocketHandler create(); } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/server/SocketLike.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.server; import android.net.LocalSocket; import com.facebook.stetho.server.CompositeInputStream; import com.facebook.stetho.server.LeakyBufferedInputStream; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; /** * Utility to allow reading buffered data from a socket and then "unreading" the data * and combining it with the original unbuffered stream. This is useful when * handing off from one logical protocol layer to the next, such as when upgrading an HTTP * connection to the websocket protocol. */ public class SocketLike { private final LocalSocket mSocket; private final LeakyBufferedInputStream mLeakyInput; public SocketLike(SocketLike socketLike, LeakyBufferedInputStream leakyInput) { this(socketLike.mSocket, leakyInput); } public SocketLike(LocalSocket socket, LeakyBufferedInputStream leakyInput) { mSocket = socket; mLeakyInput = leakyInput; } public InputStream getInput() throws IOException { return mLeakyInput.leakBufferAndStream(); } public OutputStream getOutput() throws IOException { return mSocket.getOutputStream(); } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/server/SocketLikeHandler.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.server; import android.net.LocalSocket; import java.io.IOException; /** * Similar to {@link SocketHandler} but designed to operate on {@link SocketLike} instances * which allow for buffered "peeks" of data to decide which protocol handler to use. * * @see SocketHandler * @see SocketLike */ public interface SocketLikeHandler { /** @see SocketHandler#onAccepted(LocalSocket) */ void onAccepted(SocketLike socket) throws IOException; } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/server/http/ExactPathMatcher.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.server.http; public class ExactPathMatcher implements PathMatcher { private final String mPath; public ExactPathMatcher(String path) { mPath = path; } @Override public boolean match(String path) { return mPath.equals(path); } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/server/http/HandlerRegistry.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.server.http; import java.util.ArrayList; import androidx.annotation.Nullable; public class HandlerRegistry { private final ArrayList mPathMatchers = new ArrayList<>(); private final ArrayList mHttpHandlers = new ArrayList<>(); public synchronized void register(PathMatcher path, HttpHandler handler) { mPathMatchers.add(path); mHttpHandlers.add(handler); } public synchronized boolean unregister(PathMatcher path, HttpHandler handler) { int index = mPathMatchers.indexOf(path); if (index >= 0) { if (handler == mHttpHandlers.get(index)) { mPathMatchers.remove(index); mHttpHandlers.remove(index); return true; } } return false; } @Nullable public synchronized HttpHandler lookup(String path) { for (int i = 0, N = mPathMatchers.size(); i < N; i++) { if (mPathMatchers.get(i).match(path)) { return mHttpHandlers.get(i); } } return null; } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/server/http/HttpHandler.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.server.http; import com.facebook.stetho.server.SocketLike; import java.io.IOException; public interface HttpHandler { boolean handleRequest( SocketLike socket, LightHttpRequest request, LightHttpResponse response) throws IOException; } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/server/http/HttpHeaders.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.server.http; public interface HttpHeaders { String CONTENT_TYPE = "Content-Type"; String CONTENT_LENGTH = "Content-Length"; } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/server/http/HttpStatus.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.server.http; public interface HttpStatus { int HTTP_SWITCHING_PROTOCOLS = 101; int HTTP_OK = 200; int HTTP_NOT_FOUND = 404; int HTTP_INTERNAL_SERVER_ERROR = 500; int HTTP_NOT_IMPLEMENTED = 501; } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/server/http/LightHttpBody.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.server.http; import java.io.IOException; import java.io.OutputStream; import java.io.UnsupportedEncodingException; public abstract class LightHttpBody { public static LightHttpBody create(String body, String contentType) { try { return create(body.getBytes("UTF-8"), contentType); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } } public static LightHttpBody create(final byte[] body, final String contentType) { return new LightHttpBody() { @Override public String contentType() { return contentType; } @Override public int contentLength() { return body.length; } @Override public void writeTo(OutputStream output) throws IOException { output.write(body); } }; } public abstract String contentType(); public abstract int contentLength(); public abstract void writeTo(OutputStream output) throws IOException; } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/server/http/LightHttpMessage.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.server.http; import java.util.ArrayList; import androidx.annotation.Nullable; public class LightHttpMessage { public final ArrayList headerNames = new ArrayList<>(); public final ArrayList headerValues = new ArrayList<>(); public void addHeader(String name, String value) { headerNames.add(name); headerValues.add(value); } @Nullable public String getFirstHeaderValue(String name) { for (int i = 0, N = headerNames.size(); i < N; i++) { if (name.equals(headerNames.get(i))) { return headerValues.get(i); } } return null; } public void reset() { headerNames.clear(); headerValues.clear(); } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/server/http/LightHttpRequest.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.server.http; import android.net.Uri; public class LightHttpRequest extends LightHttpMessage { public String method; public Uri uri; public String protocol; @Override public void reset() { super.reset(); this.method = null; this.uri = null; this.protocol = null; } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/server/http/LightHttpResponse.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.server.http; public class LightHttpResponse extends LightHttpMessage { public int code; public String reasonPhrase; public LightHttpBody body; public void prepare() { if (body != null) { addHeader(HttpHeaders.CONTENT_TYPE, body.contentType()); addHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(body.contentLength())); } } @Override public void reset() { super.reset(); this.code = -1; this.reasonPhrase = null; this.body = null; } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/server/http/LightHttpServer.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.server.http; import android.net.Uri; import com.facebook.stetho.server.LeakyBufferedInputStream; import com.facebook.stetho.server.SocketLike; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.EOFException; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.PrintWriter; import java.io.StringWriter; import androidx.annotation.Nullable; /** * Somewhat crude but very fast HTTP server designed exclusively to handle the * Chrome DevTools protocol, though sufficiently general to do other very basic things. * Performance is imperative here as Chrome aggressively polls Stetho asking for * meta data when the discovery window is open in Chrome. */ public class LightHttpServer { private static final String TAG = "LightHttpServer"; private final HandlerRegistry mHandlerRegistry; public LightHttpServer(HandlerRegistry handlerRegistry) { mHandlerRegistry = handlerRegistry; } public void serve(SocketLike socket) throws IOException { LeakyBufferedInputStream input = new LeakyBufferedInputStream(socket.getInput(), 1024); OutputStream output = socket.getOutput(); HttpMessageReader reader = new HttpMessageReader(input); HttpMessageWriter writer = new HttpMessageWriter(new BufferedOutputStream(output)); SocketLike anotherSocketLike = new SocketLike(socket, input); LightHttpRequest scratchRequest = new LightHttpRequest(); LightHttpResponse scratchResponse = new LightHttpResponse(); LightHttpRequest request; // This loops assumes we are always using keep-alive connections. If we're wrong, we // expect the client to just close the connection. while ((request = readRequestMessage(scratchRequest, reader)) != null) { final LightHttpResponse response = scratchResponse; response.reset(); // Note, if we're upgrading to websockets, this will block for the lifetime of the // websocket session... boolean keepGoing = dispatchToHandler(anotherSocketLike, request, response); if (!keepGoing) { // Orderly shutdown, ignore response and break the loop. break; } writeFullResponse(response, writer, output); } } private boolean dispatchToHandler( SocketLike socketLike, LightHttpRequest request, LightHttpResponse response) throws IOException { HttpHandler handler = mHandlerRegistry.lookup(request.uri.getPath()); if (handler == null) { response.code = HttpStatus.HTTP_NOT_FOUND; response.reasonPhrase = "Not found"; response.body = LightHttpBody.create("No handler found\n", "text/plain"); return true; } else { try { return handler.handleRequest(socketLike, request, response); } catch (RuntimeException e) { response.code = HttpStatus.HTTP_INTERNAL_SERVER_ERROR; response.reasonPhrase = "Internal Server Error"; StringWriter stack = new StringWriter(); PrintWriter stackWriter = new PrintWriter(stack); try { e.printStackTrace(stackWriter); } finally { stackWriter.close(); } response.body = LightHttpBody.create(stack.toString(), "text/plain"); return true; } } } @Nullable private static LightHttpRequest readRequestMessage( LightHttpRequest request, HttpMessageReader reader) throws IOException { request.reset(); String requestLine = reader.readLine(); if (requestLine == null) { return null; } // Zero tolerance on URI encoding, that URI better not have a space in it... String[] requestParts = requestLine.split(" ", 3); if (requestParts.length != 3) { throw new IOException("Invalid request line: " + requestLine); } request.method = requestParts[0]; request.uri = Uri.parse(requestParts[1]); request.protocol = requestParts[2]; readHeaders(request, reader); return request; } private static void readHeaders( LightHttpMessage message, HttpMessageReader reader) throws IOException { String headerLine; while (true) { headerLine = reader.readLine(); if (headerLine == null) { throw new EOFException(); } else if ("".equals(headerLine)) { break; } else { String[] headerParts = headerLine.split(": ", 2); if (headerParts.length != 2) { throw new IOException("Malformed header: " + headerLine); } String name = headerParts[0]; String value = headerParts[1]; message.headerNames.add(name); message.headerValues.add(value); } } } private static void writeFullResponse( LightHttpResponse response, HttpMessageWriter writer, OutputStream output) throws IOException { response.prepare(); writeResponseMessage(response, writer); if (response.body != null) { response.body.writeTo(output); } } public static void writeResponseMessage(LightHttpResponse response, HttpMessageWriter writer) throws IOException { writer.writeLine("HTTP/1.1 " + response.code + " " + response.reasonPhrase); for (int i = 0, N = response.headerNames.size(); i < N; i++) { String name = response.headerNames.get(i); String value = response.headerValues.get(i); writer.writeLine(name + ": " + value); } writer.writeLine(); writer.flush(); } /** * Efficient, unbuffered variation of {@link InputStreamReader} which assumes the input is * always ASCII. This is especially useful when you are certain that the client and server * are both mechanized and will not contain non-ASCII characters in the control messages upon * which this reader is applied. */ private static class HttpMessageReader { private final BufferedInputStream mIn; private final StringBuilder mBuffer = new StringBuilder(); private final NewLineDetector mNewLineDetector = new NewLineDetector(); public HttpMessageReader(BufferedInputStream in) { mIn = in; } @Nullable public String readLine() throws IOException { while (true) { int b = mIn.read(); if (b < 0) { return null; } char c = (char)b; mNewLineDetector.accept(c); switch (mNewLineDetector.state()) { case NewLineDetector.STATE_ON_CRLF: String result = mBuffer.toString(); mBuffer.setLength(0); return result; case NewLineDetector.STATE_ON_CR: break; case NewLineDetector.STATE_ON_OTHER: mBuffer.append(c); break; } } } private static class NewLineDetector { private static final int STATE_ON_OTHER = 1; private static final int STATE_ON_CR = 2; private static final int STATE_ON_CRLF = 3; private int state = STATE_ON_OTHER; public void accept(char c) { switch (state) { case STATE_ON_OTHER: if (c == '\r') { state = STATE_ON_CR; } break; case STATE_ON_CR: if (c == '\n') { state = STATE_ON_CRLF; } else { state = STATE_ON_OTHER; } break; case STATE_ON_CRLF: if (c == '\r') { state = STATE_ON_CR; } else { state = STATE_ON_OTHER; } break; default: throw new IllegalArgumentException("Unknown state: " + state); } } public int state() { return state; } } } /** * Similar in spirit to {@link HttpMessageReader} which assumes ASCII for all messages as * a performance optimization. Caller is responsible for flushing the writer. *

* Exposed publicly as a hack to support WebSocket upgrade. */ public static class HttpMessageWriter { private final BufferedOutputStream mOut; private static final byte[] CRLF = "\r\n".getBytes(); public HttpMessageWriter(BufferedOutputStream out) { mOut = out; } public void writeLine(String line) throws IOException { for (int i = 0, N = line.length(); i < N; i++) { char c = line.charAt(i); mOut.write((int)c); } mOut.write(CRLF); } public void writeLine() throws IOException { mOut.write(CRLF); } public void flush() throws IOException { mOut.flush(); } } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/server/http/PathMatcher.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.server.http; public interface PathMatcher { boolean match(String path); } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/server/http/RegexpPathMatcher.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.server.http; import java.util.regex.Pattern; public class RegexpPathMatcher implements PathMatcher { private final Pattern mPattern; public RegexpPathMatcher(Pattern pattern) { mPattern = pattern; } @Override public boolean match(String path) { return mPattern.matcher(path).matches(); } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/websocket/CloseCodes.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.websocket; /** * Close codes as defined by RFC6455. */ public interface CloseCodes { int NORMAL_CLOSURE = 1000; int PROTOCOL_ERROR = 1002; int CLOSED_ABNORMALLY = 1006; int UNEXPECTED_CONDITION = 1011; } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/websocket/Frame.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.websocket; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; /** * WebSocket frame as per RFC6455. */ class Frame { public static final byte OPCODE_TEXT_FRAME = 0x1; public static final byte OPCODE_BINARY_FRAME = 0x2; public static final byte OPCODE_CONNECTION_CLOSE = 0x8; public static final byte OPCODE_CONNECTION_PING = 0x9; public static final byte OPCODE_CONNECTION_PONG = 0xA; public boolean fin; public boolean rsv1; public boolean rsv2; public boolean rsv3; public byte opcode; public boolean hasMask; public long payloadLen; public byte[] maskingKey; public byte[] payloadData; public void readFrom(BufferedInputStream input) throws IOException { decodeFirstByte(readByteOrThrow(input)); byte maskAndFirstLengthBits = readByteOrThrow(input); hasMask = (maskAndFirstLengthBits & 0x80) != 0; payloadLen = decodeLength((byte)(maskAndFirstLengthBits & ~0x80), input); maskingKey = hasMask ? decodeMaskingKey(input) : null; payloadData = new byte[(int)payloadLen]; readBytesOrThrow(input, payloadData, 0, (int)payloadLen); MaskingHelper.unmask(maskingKey, payloadData, 0, (int)payloadLen); } public void writeTo(BufferedOutputStream output) throws IOException { output.write(encodeFirstByte()); byte[] lengthAndMaskBit = encodeLength(payloadLen); if (hasMask) { lengthAndMaskBit[0] |= 0x80; } output.write(lengthAndMaskBit, 0, lengthAndMaskBit.length); if (hasMask) { throw new UnsupportedOperationException("Writing masked data not implemented"); } output.write(payloadData, 0, (int) payloadLen); } private void decodeFirstByte(byte b) { fin = (b & 0x80) != 0; rsv1 = (b & 0x40) != 0; rsv2 = (b & 0x20) != 0; rsv3 = (b & 0x10) != 0; opcode = (byte)(b & 0xf); } private byte encodeFirstByte() { byte b = 0; if (fin) { b |= 0x80; } if (rsv1) { b |= 0x40; } if (rsv2) { b |= 0x20; } if (rsv3) { b |= 0x10; } b |= (opcode & 0xf); return b; } private long decodeLength(byte firstLenByte, InputStream in) throws IOException { if (firstLenByte <= 125) { return firstLenByte; } else if (firstLenByte == 126) { return (readByteOrThrow(in) & 0xff) << 8 | (readByteOrThrow(in) & 0xff); } else if (firstLenByte == 127) { long len = 0; for (int i = 0; i < 8; i++) { len <<= 8; len |= (readByteOrThrow(in) & 0xff); } return len; } else { throw new IOException("Unexpected length byte: " + firstLenByte); } } private static byte[] encodeLength(long len) { if (len <= 125) { return new byte[] { (byte)len }; } else if (len <= 0xffff) { return new byte[] { 126, (byte)((len >> 8) & 0xff), (byte)((len) & 0xff) }; } else { return new byte[] { 127, (byte)((len >> 56) & 0xff), (byte)((len >> 48) & 0xff), (byte)((len >> 40) & 0xff), (byte)((len >> 32) & 0xff), (byte)((len >> 24) & 0xff), (byte)((len >> 16) & 0xff), (byte)((len >> 8) & 0xff), (byte)((len) & 0xff) }; } } private static byte[] decodeMaskingKey(InputStream in) throws IOException { byte[] key = new byte[4]; readBytesOrThrow(in, key, 0, key.length); return key; } private static void readBytesOrThrow(InputStream in, byte[] buf, int offset, int count) throws IOException { while (count > 0) { int n = in.read(buf, offset, count); if (n == -1) { throw new EOFException(); } count -= n; offset += n; } } private static byte readByteOrThrow(InputStream in) throws IOException { int b = in.read(); if (b == -1) { throw new EOFException(); } return (byte)b; } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/websocket/FrameHelper.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.websocket; import com.facebook.stetho.common.Utf8Charset; class FrameHelper { public static Frame createTextFrame(String payload) { return createSimpleFrame(Frame.OPCODE_TEXT_FRAME, Utf8Charset.encodeUTF8(payload)); } public static Frame createBinaryFrame(byte[] payload) { return createSimpleFrame(Frame.OPCODE_BINARY_FRAME, payload); } public static Frame createCloseFrame(int closeCode, String reasonPhrase) { byte[] reasonPhraseEncoded = null; int payloadLen = 2; if (reasonPhrase != null) { reasonPhraseEncoded = Utf8Charset.encodeUTF8(reasonPhrase); payloadLen += reasonPhraseEncoded.length; } byte[] payload = new byte[payloadLen]; payload[0] = (byte)((closeCode >> 8) & 0xff); payload[1] = (byte)((closeCode) & 0xff); if (reasonPhraseEncoded != null) { System.arraycopy(reasonPhraseEncoded, 0, payload, 2, reasonPhraseEncoded.length); } return createSimpleFrame(Frame.OPCODE_CONNECTION_CLOSE, payload); } public static Frame createPingFrame(byte[] payload, int payloadLen) { return createSimpleFrame(Frame.OPCODE_CONNECTION_PING, payload, payloadLen); } public static Frame createPongFrame(byte[] payload, int payloadLen) { return createSimpleFrame(Frame.OPCODE_CONNECTION_PONG, payload, payloadLen); } private static Frame createSimpleFrame(byte opcode, byte[] payload) { return createSimpleFrame(opcode, payload, payload.length); } private static Frame createSimpleFrame(byte opCode, byte[] payload, int payloadLen) { Frame frame = new Frame(); frame.fin = true; frame.hasMask = false; frame.opcode = opCode; frame.payloadLen = payloadLen; frame.payloadData = payload; return frame; } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/websocket/MaskingHelper.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.websocket; class MaskingHelper { public static void unmask(byte[] key, byte[] data, int offset, int count) { int index = 0; while (count-- > 0) { data[offset++] ^= key[index++ % key.length]; } } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/websocket/ReadCallback.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.websocket; interface ReadCallback { void onCompleteFrame(byte opcode, byte[] payload, int payloadLen); } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/websocket/ReadHandler.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.websocket; import java.io.BufferedInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; class ReadHandler { private final BufferedInputStream mBufferedInput; private final SimpleEndpoint mEndpoint; /** * Used to build a larger payload over multiple frames. */ private final ByteArrayOutputStream mCurrentPayload = new ByteArrayOutputStream(); public ReadHandler(InputStream bufferedInput, SimpleEndpoint endpoint) { mBufferedInput = new BufferedInputStream(bufferedInput, 1024); mEndpoint = endpoint; } /** * Enter a loop processing incoming frames until orderly shutdown or a socket exception is * thrown. This method returns normally on orderly shutdown, throws otherwise. * * @throws IOException Socket exception during the read loop. */ public void readLoop(ReadCallback readCallback) throws IOException { Frame frame = new Frame(); do { frame.readFrom(mBufferedInput); mCurrentPayload.write(frame.payloadData, 0, (int)frame.payloadLen); if (frame.fin) { byte[] completePayload = mCurrentPayload.toByteArray(); readCallback.onCompleteFrame(frame.opcode, completePayload, completePayload.length); mCurrentPayload.reset(); } } while (frame.opcode != Frame.OPCODE_CONNECTION_CLOSE); } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/websocket/SimpleEndpoint.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.websocket; /** * Alternative to JSR-356's Endpoint class but with a less insane J2EE-style API. */ public interface SimpleEndpoint { /** * Invoked when a new WebSocket session is established. * * @param session Unique handle for this session. */ void onOpen(SimpleSession session); /** * Invoked when a text-based message is received from the peer. May have spanned multiple * WebSocket packets. * * @param session Unique handle for this session. * @param message Complete payload data. */ void onMessage(SimpleSession session, String message); /** * Invoked when a binary message is received from the peer. May have spanned multiple * WebSocket packets. * * @param session Unique handle for this session. * @param message Complete payload data. * @param messageLen Maximum number of bytes of {@code message} to read. */ void onMessage(SimpleSession session, byte[] message, int messageLen); /** * Invoked when a remote peer closed the WebSocket session or if {@link SimpleSession#close} * is invoked on our side. * * @param session Unique handle for this session. * @param closeReasonCode Close reason code (see RFC6455) * @param closeReasonPhrase Possibly arbitrary text phrase associated with the reason code. */ void onClose(SimpleSession session, int closeReasonCode, String closeReasonPhrase); /** * Invoked when errors occur out of the normal band of the WebSocket protocol. This is * intended for debug purposes and is generally not actionable. The {@link #onClose} method * will still be invoked in all cases, making it reasonable to simply log in this method. * * @param session Unique handle for this session. * @param t Exception that occurred. */ void onError(SimpleSession session, Throwable t); } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/websocket/SimpleSession.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.websocket; /** * Alternative to JSR-356's Session class but with a less insane J2EE-style API. */ public interface SimpleSession { void sendText(String payload); void sendBinary(byte[] payload); /** * Request that the session be closed. * * @param closeReason Close reason, as per RFC6455 * @param reasonPhrase Possibly arbitrary close reason phrase. */ void close(int closeReason, String reasonPhrase); boolean isOpen(); } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/websocket/WebSocketHandler.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.websocket; import android.util.Base64; import com.facebook.stetho.common.Utf8Charset; 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.LightHttpMessage; import com.facebook.stetho.server.http.LightHttpRequest; import com.facebook.stetho.server.http.LightHttpResponse; import com.facebook.stetho.server.http.LightHttpServer; import javax.annotation.Nullable; import java.io.BufferedOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; /** * Crazy kludge to support upgrading to the WebSocket protocol while still using the * {@link HttpHandler} harness. *

* The way this works is that we pump the request directly into our WebSocket implementation and * force write the response out to the connection without returning. Then, we extract the * remaining buffered input stream bytes from the socket and stitch them together with the * raw sockets input stream and pass everything onto the WebSocket engine which blocks * until WebSocket orderly shutdown. */ public class WebSocketHandler implements HttpHandler { private static final String HEADER_UPGRADE = "Upgrade"; private static final String HEADER_CONNECTION = "Connection"; private static final String HEADER_SEC_WEBSOCKET_KEY = "Sec-WebSocket-Key"; private static final String HEADER_SEC_WEBSOCKET_ACCEPT = "Sec-WebSocket-Accept"; private static final String HEADER_SEC_WEBSOCKET_PROTOCOL = "Sec-WebSocket-Protocol"; private static final String HEADER_SEC_WEBSOCKET_VERSION = "Sec-WebSocket-Version"; private static final String HEADER_UPGRADE_WEBSOCKET = "websocket"; private static final String HEADER_CONNECTION_UPGRADE = "Upgrade"; private static final String HEADER_SEC_WEBSOCKET_VERSION_13 = "13"; // Are you kidding me? The WebSocket spec requires that we append this weird hardcoded String // to the key we receive from the client, SHA-1 that, and base64 encode it back to the client. // I'm guessing this is to prevent replay attacks of some kind but given that there's no actual // security context here, I can only imagine that this is just security through obscurity in // some fashion. private static final String SERVER_KEY_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; private final SimpleEndpoint mEndpoint; public WebSocketHandler(SimpleEndpoint endpoint) { mEndpoint = endpoint; } @Override public boolean handleRequest( SocketLike socket, LightHttpRequest request, LightHttpResponse response) throws IOException { if (!isSupportableUpgradeRequest(request)) { response.code = HttpStatus.HTTP_NOT_IMPLEMENTED; response.reasonPhrase = "Not Implemented"; response.body = LightHttpBody.create( "Not a supported WebSocket upgrade request\n", "text/plain"); return true; } // This will not return on successful WebSocket upgrade, but rather block until the session is // shut down or a socket error occurs. doUpgrade(socket, request, response); return false; } private static boolean isSupportableUpgradeRequest(LightHttpRequest request) { return HEADER_UPGRADE_WEBSOCKET.equalsIgnoreCase(getFirstHeaderValue(request, HEADER_UPGRADE)) && HEADER_CONNECTION_UPGRADE.equals(getFirstHeaderValue(request, HEADER_CONNECTION)) && HEADER_SEC_WEBSOCKET_VERSION_13.equals( getFirstHeaderValue(request, HEADER_SEC_WEBSOCKET_VERSION)); } private void doUpgrade( SocketLike socketLike, LightHttpRequest request, LightHttpResponse response) throws IOException { response.code = HttpStatus.HTTP_SWITCHING_PROTOCOLS; response.reasonPhrase = "Switching Protocols"; response.addHeader(HEADER_UPGRADE, HEADER_UPGRADE_WEBSOCKET); response.addHeader(HEADER_CONNECTION, HEADER_CONNECTION_UPGRADE); response.body = null; String clientKey = getFirstHeaderValue(request, HEADER_SEC_WEBSOCKET_KEY); if (clientKey != null) { response.addHeader(HEADER_SEC_WEBSOCKET_ACCEPT, generateServerKey(clientKey)); } InputStream in = socketLike.getInput(); OutputStream out = socketLike.getOutput(); LightHttpServer.writeResponseMessage( response, new LightHttpServer.HttpMessageWriter(new BufferedOutputStream(out))); WebSocketSession session = new WebSocketSession(in, out, mEndpoint); session.handle(); } private static String generateServerKey(String clientKey) { try { String serverKey = clientKey + SERVER_KEY_GUID; MessageDigest sha1 = MessageDigest.getInstance("SHA-1"); sha1.update(Utf8Charset.encodeUTF8(serverKey)); return Base64.encodeToString(sha1.digest(), Base64.NO_WRAP); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } } @Nullable private static String getFirstHeaderValue(LightHttpMessage message, String headerName) { return message.getFirstHeaderValue(headerName); } } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/websocket/WebSocketSession.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.websocket; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.concurrent.atomic.AtomicBoolean; /** * Binding driver between raw socket I/O and a high-level WebSocket interface. This implementation * is generally very weak and doesn't offer sensible optimizations such as re-used buffers, * efficient UTF-8 encoding/decoding, or the full spectrum of features defined in the RFC. */ class WebSocketSession implements SimpleSession { private final ReadHandler mReadHandler; private final WriteHandler mWriteHandler; private final SimpleEndpoint mEndpoint; private AtomicBoolean mIsOpen = new AtomicBoolean(false); private volatile boolean mSentClose; public WebSocketSession( InputStream rawSocketInput, OutputStream rawSocketOutput, SimpleEndpoint endpoint) { mReadHandler = new ReadHandler(rawSocketInput, endpoint); mWriteHandler = new WriteHandler(rawSocketOutput); mEndpoint = endpoint; } public void handle() throws IOException { markAndSignalOpen(); // Loop until orderly shutdown or socket exception. try { mReadHandler.readLoop(mReadCallback); } catch (EOFException e) { // No need to rethrow, this can be considered a graceful shutdown of the socket (though // not the WebSocket). markAndSignalClosed(CloseCodes.UNEXPECTED_CONDITION, "EOF while reading"); } catch (IOException e) { markAndSignalClosed(CloseCodes.CLOSED_ABNORMALLY, null /* reasonPhrase */); throw e; } } @Override public void sendText(String payload) { doWrite(FrameHelper.createTextFrame(payload)); } @Override public void sendBinary(byte[] payload) { doWrite(FrameHelper.createBinaryFrame(payload)); } @Override public void close(int closeReason, String reasonPhrase) { sendClose(closeReason, reasonPhrase); markAndSignalClosed(closeReason, reasonPhrase); } private void sendClose(int closeReason, String reasonPhrase) { doWrite(FrameHelper.createCloseFrame(closeReason, reasonPhrase)); markSentClose(); } void markSentClose() { mSentClose = true; } void markAndSignalOpen() { if (!mIsOpen.getAndSet(true)) { mEndpoint.onOpen(this /* session */); } } void markAndSignalClosed(int closeReason, String reasonPhrase) { if (mIsOpen.getAndSet(false)) { mEndpoint.onClose(this /* session */, closeReason, reasonPhrase); } } @Override public boolean isOpen() { return mIsOpen.get(); } private void doWrite(Frame frame) { if (signalErrorIfNotOpen()) { return; } mWriteHandler.write(frame, mErrorForwardingWriteCallback); } /** * Signals an error to the {@link SimpleEndpoint} if the session is closed. * * @return True if an error was signaled (the session is closed); false otherwise. */ private boolean signalErrorIfNotOpen() { if (!isOpen()) { signalError(new IOException("Session is closed")); return true; } return false; } private void signalError(IOException e) { mEndpoint.onError(this /* session */, e); } private final ReadCallback mReadCallback = new ReadCallback() { @Override public void onCompleteFrame(byte opcode, byte[] payload, int payloadLen) { switch (opcode) { case Frame.OPCODE_CONNECTION_CLOSE: handleClose(payload, payloadLen); break; case Frame.OPCODE_CONNECTION_PING: handlePing(payload, payloadLen); break; case Frame.OPCODE_CONNECTION_PONG: handlePong(payload, payloadLen); break; case Frame.OPCODE_TEXT_FRAME: handleTextFrame(payload, payloadLen); break; case Frame.OPCODE_BINARY_FRAME: handleBinaryFrame(payload, payloadLen); break; default: signalError(new IOException("Unsupported frame opcode=" + opcode)); break; } } private void handleClose(byte[] payload, int payloadLen) { int closeCode; String closeReasonPhrase; if (payloadLen >= 2) { closeCode = ((payload[0] & 0xff) << 8) | (payload[1] & 0xff); closeReasonPhrase = (payloadLen > 2) ? new String(payload, 2, payloadLen - 2) : null; } else { closeCode = CloseCodes.CLOSED_ABNORMALLY; closeReasonPhrase = "Unparseable close frame"; } // We must acknowledge the peer's close frame. if (!mSentClose) { sendClose(CloseCodes.NORMAL_CLOSURE, "Received close frame"); } markAndSignalClosed(closeCode, closeReasonPhrase); } private void handlePing(byte[] payload, int payloadLen) { doWrite(FrameHelper.createPongFrame(payload, payloadLen)); } private void handlePong(byte[] payload, int payloadLen) { // Great, whatever... } private void handleTextFrame(byte[] payload, int payloadLen) { mEndpoint.onMessage(WebSocketSession.this, new String(payload, 0, payloadLen)); } private void handleBinaryFrame(byte[] payload, int payloadLen) { mEndpoint.onMessage(WebSocketSession.this, payload, payloadLen); } }; private final WriteCallback mErrorForwardingWriteCallback = new WriteCallback() { @Override public void onFailure(IOException e) { signalError(e); } @Override public void onSuccess() { // Boring... } }; } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/websocket/WriteCallback.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.websocket; import java.io.IOException; interface WriteCallback { void onFailure(IOException e); void onSuccess(); } ================================================ FILE: stetho/src/main/java/com/facebook/stetho/websocket/WriteHandler.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.websocket; import javax.annotation.concurrent.ThreadSafe; import java.io.BufferedOutputStream; import java.io.IOException; import java.io.OutputStream; @ThreadSafe class WriteHandler { private final BufferedOutputStream mBufferedOutput; public WriteHandler(OutputStream rawSocketOutput) { mBufferedOutput = new BufferedOutputStream(rawSocketOutput, 1024); } public synchronized void write(Frame frame, WriteCallback callback) { try { frame.writeTo(mBufferedOutput); mBufferedOutput.flush(); callback.onSuccess(); } catch (IOException e) { callback.onFailure(e); } } } ================================================ FILE: stetho/src/test/java/com/facebook/stetho/PluginBuilderTest.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho; import android.app.Activity; import android.os.Build; import com.facebook.stetho.dumpapp.DumperPlugin; import com.facebook.stetho.dumpapp.plugins.HprofDumperPlugin; import com.facebook.stetho.inspector.protocol.ChromeDevtoolsDomain; import com.facebook.stetho.inspector.protocol.module.Debugger; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.Robolectric; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; import java.io.IOException; import static org.junit.Assert.assertFalse; @Config(emulateSdk = Build.VERSION_CODES.JELLY_BEAN) @RunWith(RobolectricTestRunner.class) public class PluginBuilderTest { private final Activity mActivity = Robolectric.setupActivity(Activity.class); @Test public void test_Remove_DefaultInspectorModulesBuilder() throws IOException { final Class debuggerClass = Debugger.class; Iterable domains = new Stetho.DefaultInspectorModulesBuilder(mActivity) .remove(debuggerClass.getName()) .finish(); boolean containsDebugggerDomain = false; for (ChromeDevtoolsDomain domain : domains) { if (domain.getClass().equals(debuggerClass)) { containsDebugggerDomain = true; break; } } assertFalse(containsDebugggerDomain); } @Test public void test_Remove_DefaultDumperPluginsBuilder() throws IOException { //HprofDumperPlugin.NAME is private final String hprofDumperPluginNAME = "hprof"; final Iterable dumperPlugins = new Stetho.DefaultDumperPluginsBuilder(mActivity) .remove(hprofDumperPluginNAME) .finish(); boolean containsDebugggerDomain = false; for (DumperPlugin plugin : dumperPlugins) { if (plugin.getClass().equals(HprofDumperPlugin.class)) { containsDebugggerDomain = true; break; } } assertFalse(containsDebugggerDomain); } } ================================================ FILE: stetho/src/test/java/com/facebook/stetho/inspector/database/DatabasePeerManagerTest.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.database; import java.io.File; import java.util.Arrays; import java.util.List; import org.junit.Test; import static org.junit.Assert.assertArrayEquals; public class DatabasePeerManagerTest { @Test public void testTidyDatabaseList() { File[] databases = { new File("foo.db"), new File("foo.db-journal"), new File("bar.db"), new File("bar.db-journal"), new File( "bar.db-uid"), new File("baz.db"), new File("baz.db-somethingelse"), new File("dangling.db-journal"), }; File[] expected = { new File( "foo.db"), new File("bar.db"), new File("baz.db"), new File("baz.db-somethingelse"), new File("dangling.db-journal") }; List tidied = SqliteDatabaseDriver.tidyDatabaseList(Arrays.asList(databases)); assertArrayEquals(expected, tidied.toArray()); } } ================================================ FILE: stetho/src/test/java/com/facebook/stetho/inspector/elements/android/MethodInvokerTest.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.elements.android; import android.app.Activity; import android.os.Build; import android.widget.CheckBox; import android.widget.TextView; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.Robolectric; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; import static org.junit.Assert.assertEquals; @Config(emulateSdk = Build.VERSION_CODES.JELLY_BEAN) @RunWith(RobolectricTestRunner.class) public class MethodInvokerTest { private final Activity mActivity = Robolectric.setupActivity(Activity.class); private final TextView mTextView = new TextView(mActivity); private final CheckBox mCheckBox = new CheckBox(mActivity); private final MethodInvoker mInvoker = new MethodInvoker(); @Before public void setup() { } @Test public void testSetCharSequence() { mInvoker.invoke(mTextView, "setText", "Hello World"); assertEquals("Hello World", mTextView.getText().toString()); } @Test public void testSetInteger() { mInvoker.invoke(mTextView, "setId", "2"); assertEquals(2, mTextView.getId()); } @Test public void testSetFloat() { mInvoker.invoke(mTextView, "setTextSize", "34"); assertEquals(34f, mTextView.getTextSize(), 0); } @Test public void testSetBoolean() { mInvoker.invoke(mCheckBox, "setChecked", "true"); assertEquals(true, mCheckBox.isChecked()); } @Test public void testSetAttributeAsTextIgnoreUnknownAttribute() { // Should not throw mInvoker.invoke(mTextView, "setSomething", "foo"); } } ================================================ FILE: stetho/src/test/java/com/facebook/stetho/inspector/elements/android/ViewDescriptorTest.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.elements.android; import android.app.Activity; import android.os.Build; import android.widget.CheckBox; import android.widget.TextView; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.Robolectric; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; import static org.mockito.Matchers.anyObject; import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @Config(emulateSdk = Build.VERSION_CODES.JELLY_BEAN) @RunWith(RobolectricTestRunner.class) public class ViewDescriptorTest { private final MethodInvoker mMethodInvoker = mock(MethodInvoker.class); private final ViewDescriptor mDescriptor = new ViewDescriptor(mMethodInvoker); private final Activity mActivity = Robolectric.setupActivity(Activity.class); private final TextView mTextView = new TextView(mActivity); private final CheckBox mCheckBox = new CheckBox(mActivity); @Test public void testSetAttributeAsTextWithSetText() { mDescriptor.setAttributesAsText(mTextView, "text=\"Hello World\""); verify(mMethodInvoker).invoke(mTextView, "setText", "Hello World"); } @Test public void testSetAttributeAsTextWithSetId() { mDescriptor.setAttributesAsText(mTextView, "id=\"2\""); verify(mMethodInvoker).invoke(mTextView, "setId", "2"); } @Test public void testSetAttributeAsTextWithSetChecked() { mDescriptor.setAttributesAsText(mCheckBox, "checked=\"true\""); verify(mMethodInvoker).invoke(mCheckBox, "setChecked", "true"); } @Test public void testSetMultipleAttributesAsText() { mDescriptor.setAttributesAsText(mTextView, "id=\"2\" text=\"Hello World\""); verify(mMethodInvoker).invoke(mTextView, "setId", "2"); verify(mMethodInvoker).invoke(mTextView, "setText", "Hello World"); } @Test public void testSetAttributeAsTextIgnoreInvalidFormat() { mDescriptor.setAttributesAsText(mTextView, "garbage"); verify(mMethodInvoker, never()).invoke(anyObject(), anyString(), anyString()); } } ================================================ FILE: stetho/src/test/java/com/facebook/stetho/inspector/network/AsyncPrettyPrintResponseBodyTest.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.network; import com.facebook.stetho.common.Util; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.times; import static org.mockito.Mockito.never; import static org.mockito.Matchers.any; import com.facebook.stetho.inspector.network.AsyncPrettyPrinter; import com.facebook.stetho.inspector.network.AsyncPrettyPrinterExecutorHolder; import dalvik.annotation.TestTargetClass; import org.junit.Before; import org.junit.Test; import javax.annotation.Nullable; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; import java.io.StringWriter; import java.lang.Override; import java.lang.String; import java.util.ArrayList; import java.util.Arrays; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.fail; public class AsyncPrettyPrintResponseBodyTest { private static final String TEST_REQUEST_ID = "1234"; private static final String TEST_HEADER_NAME = "header name"; private static final String TEST_HEADER_VALUE = "header value"; private static final String PRETTY_PRINT_PREFIX = "pretty printed result: "; private static final String[] UNREGISTERED_HEADER_NAMES = {"unregistered header name 1", "unregistered header name 2", "unregistered header name 3"}; private static final String[] UNREGISTERED_HEADER_VALUES = {"unregistered header value 1", "unregistered header value 2", "unregistered header value 3"}; private static final byte[] TEST_RESPONSE_BODY; private static final ByteArrayInputStream mInputStream; static { int responseBodyLength = 4096 * 2 + 2048; // span multiple buffers when tee-ing TEST_RESPONSE_BODY = new byte[responseBodyLength]; for (int i = 0; i < responseBodyLength; i++) { TEST_RESPONSE_BODY[i] = positionToByte(i); } mInputStream = new ByteArrayInputStream(TEST_RESPONSE_BODY); } private AsyncPrettyPrinterRegistry mAsyncPrettyPrinterRegistry; private PrettyPrinterTestFactory mPrettyPrinterTestFactory; private ResponseBodyFileManager mResponseBodyFileManager; @Before public void setup() { mPrettyPrinterTestFactory = new PrettyPrinterTestFactory(); mResponseBodyFileManager = mock(ResponseBodyFileManager.class); mAsyncPrettyPrinterRegistry = new AsyncPrettyPrinterRegistry(); mAsyncPrettyPrinterRegistry.register(TEST_HEADER_NAME, mPrettyPrinterTestFactory); AsyncPrettyPrinterExecutorHolder.ensureInitialized(); } @Test public void testAsyncPrettyPrinterResult() throws IOException { StringWriter out = new StringWriter(); PrintWriter writer = new PrintWriter(out); AsyncPrettyPrinter mAsyncPrettyPrinter = mPrettyPrinterTestFactory.getInstance( TEST_HEADER_NAME, TEST_HEADER_VALUE); mAsyncPrettyPrinter.printTo(writer, mInputStream); assertEquals(PRETTY_PRINT_PREFIX + Arrays.toString(TEST_RESPONSE_BODY), out.toString()); } @Test public void testInitAsyncPrettyPrinterForResponseWithRegisteredHeader() { ArrayList headerNames = new ArrayList(); ArrayList headerValues = new ArrayList(); headerNames.add(UNREGISTERED_HEADER_NAMES[0]); headerNames.add(UNREGISTERED_HEADER_NAMES[1]); headerNames.add(TEST_HEADER_NAME); headerValues.add(UNREGISTERED_HEADER_VALUES[0]); headerValues.add(UNREGISTERED_HEADER_VALUES[1]); headerValues.add(TEST_HEADER_VALUE); TestInspectorResponse testResponse = new TestInspectorResponse( headerNames, headerValues, TEST_REQUEST_ID ); AsyncPrettyPrinter prettyPrinter = NetworkEventReporterImpl.createPrettyPrinterForResponse( testResponse, mAsyncPrettyPrinterRegistry); assertNotNull(prettyPrinter); } @Test public void testInitAsyncPrettyPrinterForResponseWithUnregisteredHeader() { ArrayList headerNames = new ArrayList(); ArrayList headerValues = new ArrayList(); headerNames.add(UNREGISTERED_HEADER_NAMES[0]); headerNames.add(UNREGISTERED_HEADER_NAMES[1]); headerNames.add(UNREGISTERED_HEADER_NAMES[2]); headerValues.add(UNREGISTERED_HEADER_VALUES[0]); headerValues.add(UNREGISTERED_HEADER_VALUES[1]); headerValues.add(UNREGISTERED_HEADER_VALUES[2]); TestInspectorResponse testResponse = new TestInspectorResponse( headerNames, headerValues, TEST_REQUEST_ID ); AsyncPrettyPrinter prettyPrinter = NetworkEventReporterImpl.createPrettyPrinterForResponse( testResponse, mAsyncPrettyPrinterRegistry); assertEquals(null, prettyPrinter); } @Test public void testGetInstanceWithUnmatchedHeader() { ArrayList headerNames = new ArrayList(); ArrayList headerValues = new ArrayList(); headerNames.add(UNREGISTERED_HEADER_NAMES[0]); headerNames.add(UNREGISTERED_HEADER_NAMES[1]); headerNames.add(TEST_HEADER_NAME); headerValues.add(UNREGISTERED_HEADER_VALUES[0]); headerValues.add(UNREGISTERED_HEADER_VALUES[1]); headerValues.add(UNREGISTERED_HEADER_VALUES[2]); TestInspectorResponse testResponse = new TestInspectorResponse( headerNames, headerValues, TEST_REQUEST_ID ); AsyncPrettyPrinter prettyPrinter = NetworkEventReporterImpl.createPrettyPrinterForResponse( testResponse, mAsyncPrettyPrinterRegistry); assertEquals(null, prettyPrinter); } private class PrettyPrinterTestFactory extends DownloadingAsyncPrettyPrinterFactory { @Override protected void doPrint(PrintWriter output, InputStream payload, String schema) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); Util.copy(payload, out, new byte[1024]); String prettifiedContent = PRETTY_PRINT_PREFIX + Arrays.toString(out.toByteArray()); output.write(prettifiedContent); output.close(); } @Override @Nullable protected MatchResult matchAndParseHeader(String headerName, String headerValue) { if (headerName.equals(TEST_HEADER_NAME) && headerValue.equals(TEST_HEADER_VALUE)) { return new MatchResult("https://www.facebook.com", PrettyPrinterDisplayType.TEXT); } else { return null; } } } private class TestInspectorResponse implements NetworkEventReporter.InspectorResponse { private ArrayList mHeaderNames; private ArrayList mHeaderValues; private String mRequestId; public TestInspectorResponse( ArrayList headerNames, ArrayList headerValues, String requestId) { mHeaderNames = headerNames; mHeaderValues = headerValues; mRequestId = requestId; } public int headerCount() { return mHeaderNames.size(); } public String headerName(int index) { return mHeaderNames.get(index); } public String headerValue(int index) { return mHeaderValues.get(index); } @Nullable public String firstHeaderValue(String name) { return mHeaderValues.get(0); } public String requestId() { return mRequestId; } public String url() { return "test url"; } public int statusCode() { return 200; } public String reasonPhrase() { return "test reason phrase"; } public boolean connectionReused() { return false; } public int connectionId() { return 111; } public boolean fromDiskCache() { return false; } } /** * Returns the truncated byte value of position. */ private static byte positionToByte(int position) { return (byte) (position % 0xff); } } ================================================ FILE: stetho/src/test/java/com/facebook/stetho/inspector/network/GunzippingOutputStreamTest.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.network; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.zip.GZIPOutputStream; import static org.junit.Assert.*; @RunWith(JUnit4.class) public class GunzippingOutputStreamTest { @Test(timeout = 1000) public void testGunzip() throws IOException { byte[] data = "test123test123".getBytes(); ByteArrayOutputStream out = new ByteArrayOutputStream(); OutputStream unzippingStream = GunzippingOutputStream.create(out); OutputStream zippingStream = new GZIPOutputStream(unzippingStream); zippingStream.write(data); zippingStream.close(); assertArrayEquals(data, out.toByteArray()); } } ================================================ FILE: stetho/src/test/java/com/facebook/stetho/inspector/network/ResponseHandlingInputStreamTest.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.inspector.network; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.Arrays; import com.facebook.stetho.inspector.console.CLog; import com.facebook.stetho.inspector.helper.ChromePeerManager; import com.facebook.stetho.inspector.protocol.module.Console; import static org.mockito.Mockito.mock; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mockito; import org.powermock.api.mockito.PowerMockito; import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; @RunWith(PowerMockRunner.class) @PrepareForTest(CLog.class) public class ResponseHandlingInputStreamTest { private static final String TEST_REQUEST_ID = "1234"; private static final byte[] TEST_RESPONSE_BODY; static { int responseBodyLength = 4096 * 2 + 2048; // span multiple buffers when tee-ing TEST_RESPONSE_BODY = new byte[responseBodyLength]; for (int i = 0; i < responseBodyLength; i++) { TEST_RESPONSE_BODY[i] = positionToByte(i); } } private ByteArrayOutputStream mTestOutputStream; private ResponseHandlingInputStream mResponseHandlingInputStream; private NetworkPeerManager mNetworkPeerManager; private NetworkEventReporterImpl mNetworkEventReporter; @Before public void setup() { mTestOutputStream = new ByteArrayOutputStream(); // The only place this is used is when trying to write to the console. Since we are going to // mock ResponseHandlingInputStream#writeToConsole passing null is fine. mNetworkPeerManager = null; mNetworkEventReporter = mock(NetworkEventReporterImpl.class); mResponseHandlingInputStream = new ResponseHandlingInputStream( new ByteArrayInputStream(TEST_RESPONSE_BODY), TEST_REQUEST_ID, mTestOutputStream, null /* decompressedCounter */, mNetworkPeerManager, new DefaultResponseHandler(mNetworkEventReporter, TEST_REQUEST_ID)); } @Test public void testReadOneByte() throws IOException { int result = mResponseHandlingInputStream.read(); assertEquals(TEST_RESPONSE_BODY[0], positionToByte(result)); assertBufferMatchesResponseBody(mTestOutputStream.toByteArray(), 1); PowerMockito.mockStatic(CLog.class); PowerMockito.doNothing().when(CLog.class); CLog.writeToConsole( Mockito.any(ChromePeerManager.class), Mockito.any(Console.MessageLevel.class), Mockito.any(Console.MessageSource.class), Mockito.anyString()); mResponseHandlingInputStream.close(); PowerMockito.verifyStatic(); } @Test public void testReadPartial() throws IOException { int numBytesToRead = TEST_RESPONSE_BODY.length / 2; byte[] tempReadingBuffer = new byte[numBytesToRead]; int result = mResponseHandlingInputStream.read(tempReadingBuffer, 0, numBytesToRead); assertEquals(numBytesToRead, result); assertBufferMatchesResponseBody(tempReadingBuffer, numBytesToRead); assertBufferMatchesResponseBody(mTestOutputStream.toByteArray(), numBytesToRead); PowerMockito.mockStatic(CLog.class); PowerMockito.doNothing().when(CLog.class); CLog.writeToConsole( Mockito.any(ChromePeerManager.class), Mockito.any(Console.MessageLevel.class), Mockito.any(Console.MessageSource.class), Mockito.anyString()); mResponseHandlingInputStream.close(); PowerMockito.verifyStatic(); } @Test public void testReadFully() throws IOException { byte[] tempReadingBuffer = new byte[TEST_RESPONSE_BODY.length]; int result = mResponseHandlingInputStream.read(tempReadingBuffer); assertEquals(TEST_RESPONSE_BODY.length, result); assertBufferMatchesResponseBody(tempReadingBuffer, TEST_RESPONSE_BODY.length); assertBufferMatchesResponseBody(mTestOutputStream.toByteArray(), TEST_RESPONSE_BODY.length); PowerMockito.mockStatic(CLog.class); PowerMockito.verifyZeroInteractions(CLog.class); mResponseHandlingInputStream.close(); PowerMockito.verifyStatic(); } @Test public void testSkipFew() throws IOException { long numBytesToSkip = TEST_RESPONSE_BODY.length / 2; long result = mResponseHandlingInputStream.skip(numBytesToSkip); assertEquals(numBytesToSkip, result); assertBufferMatchesResponseBody(mTestOutputStream.toByteArray(), (int) numBytesToSkip); PowerMockito.mockStatic(CLog.class); PowerMockito.doNothing().when(CLog.class); CLog.writeToConsole( Mockito.any(ChromePeerManager.class), Mockito.any(Console.MessageLevel.class), Mockito.any(Console.MessageSource.class), Mockito.anyString()); mResponseHandlingInputStream.close(); PowerMockito.verifyStatic(); } @Test public void testSkipMany() throws IOException { long numBytesToSkip = TEST_RESPONSE_BODY.length * 2; long result = mResponseHandlingInputStream.skip(numBytesToSkip); assertEquals((long) TEST_RESPONSE_BODY.length, result); assertBufferMatchesResponseBody(mTestOutputStream.toByteArray(), TEST_RESPONSE_BODY.length); PowerMockito.verifyZeroInteractions(CLog.class); mResponseHandlingInputStream.close(); } private static final class TestIOException extends IOException {} @Test public void testSwallowException() throws IOException { OutputStream exceptionOutputStream = new OutputStream() { @Override public void write(int oneByte) throws IOException { throw new TestIOException(); } }; ResponseHandlingInputStream responseHandlingInputStream = new ResponseHandlingInputStream( new ByteArrayInputStream(TEST_RESPONSE_BODY), TEST_REQUEST_ID, exceptionOutputStream, null /* decompressedCounter */, mNetworkPeerManager, new DefaultResponseHandler(mNetworkEventReporter, TEST_REQUEST_ID)); PowerMockito.mockStatic(CLog.class); responseHandlingInputStream.read(); PowerMockito.verifyStatic(); } /** * Returns the truncated byte value of position. */ private static byte positionToByte(int position) { return (byte) (position % 0xff); } /** * Asserts that buffer's length equal to count and matches the first count bytes of the * test response body. */ private static void assertBufferMatchesResponseBody(byte[] buffer, int count) { assertArrayEquals(Arrays.copyOf(TEST_RESPONSE_BODY, count), buffer); } } ================================================ FILE: stetho/src/test/java/com/facebook/stetho/json/ObjectMapperTest.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.json; import android.os.Build; import com.facebook.stetho.json.annotation.JsonProperty; import com.facebook.stetho.json.annotation.JsonValue; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.ListIterator; import java.util.Objects; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; /** * Tests for {@link ObjectMapper} */ @Config(emulateSdk = Build.VERSION_CODES.JELLY_BEAN) @RunWith(RobolectricTestRunner.class) public class ObjectMapperTest { private ObjectMapper mObjectMapper; @Before public void setup() { mObjectMapper = new ObjectMapper(); } @Test public void testJsonProperty() throws IOException, JSONException { JsonPropertyString c = new JsonPropertyString(); c.testString = "test"; String expected = "{\"testString\":\"test\"}"; JSONObject jsonObject = mObjectMapper.convertValue(c, JSONObject.class); String str = jsonObject.toString(); assertEquals(expected, str); JsonPropertyString jsonPropertyString = mObjectMapper.convertValue( new JSONObject(expected), JsonPropertyString.class); assertEquals(c, jsonPropertyString); } @Test public void testNestedProperty() throws JSONException { NestedJsonProperty njp = new NestedJsonProperty(); njp.child1 = new JsonPropertyString(); njp.child2 = new JsonPropertyInt(); njp.child1.testString = "testString"; njp.child2.i = 4; // The ordering of serialization changes depending on Java 7 vs Java 8. String expected7 = "{\"child1\":{\"testString\":\"testString\"},\"child2\":{\"i\":4}}"; String expected8 = "{\"child2\":{\"i\":4},\"child1\":{\"testString\":\"testString\"}}"; NestedJsonProperty parsed7 = mObjectMapper.convertValue( new JSONObject(expected7), NestedJsonProperty.class); assertEquals(njp, parsed7); NestedJsonProperty parsed8 = mObjectMapper.convertValue( new JSONObject(expected8), NestedJsonProperty.class); assertEquals(njp, parsed8); JSONObject jsonObject = mObjectMapper.convertValue(njp, JSONObject.class); assertTrue(expected7.equals(jsonObject.toString()) || expected8.equals(jsonObject.toString())); } @Test public void testEnumProperty() throws JSONException { JsonPropertyEnum jpe = new JsonPropertyEnum(); jpe.enumValue = TestEnum.VALUE_TWO; String expected = "{\"enumValue\":\"two\"}"; JsonPropertyEnum parsed = mObjectMapper.convertValue( new JSONObject(expected), JsonPropertyEnum.class); assertEquals(jpe, parsed); JSONObject jsonObject = mObjectMapper.convertValue(jpe, JSONObject.class); assertEquals(expected, jsonObject.toString()); } @Test public void testListString() throws JSONException { JsonPropertyStringList jpsl = new JsonPropertyStringList(); List values = new ArrayList(); jpsl.stringList = values; values.add("one"); values.add("two"); values.add("three"); String expected = "{\"stringList\":[\"one\",\"two\",\"three\"]}"; JsonPropertyStringList jsonPropertyStringList = mObjectMapper.convertValue( new JSONObject(expected), JsonPropertyStringList.class); assertEquals(jpsl, jsonPropertyStringList); JSONObject jsonObject = mObjectMapper.convertValue(jpsl, JSONObject.class); String str = jsonObject.toString(); assertEquals(expected, str); } @Test public void testSerializeMultitypedList() throws JSONException { List list = new ArrayList(); list.add("foo"); list.add(Collections.singletonList("bar")); JsonPropertyMultitypedList javaObj = new JsonPropertyMultitypedList(); javaObj.multitypedList = list; String expected = "{\"multitypedList\":[\"foo\",[\"bar\"]]}"; JSONObject jsonObj = mObjectMapper.convertValue(javaObj, JSONObject.class); String str = jsonObj.toString(); assertEquals(expected, str); } @Test public void testSerializeListOfLists() throws JSONException { List> listOfLists = new ArrayList>(); listOfLists.add(Collections.singletonList("foo")); ArrayList sublist2 = new ArrayList(); sublist2.add("1"); sublist2.add("2"); listOfLists.add(sublist2); JsonPropertyListOfLists javaObj = new JsonPropertyListOfLists(); javaObj.listOfLists = listOfLists; String expected = "{\"listOfLists\":[[\"foo\"],[\"1\",\"2\"]]}"; JSONObject jsonObj = mObjectMapper.convertValue(javaObj, JSONObject.class); String str = jsonObj.toString(); assertEquals(expected, str); } @Test public void testObjectToPrimitive() throws JSONException { ArrayOfPrimitivesContainer container = new ArrayOfPrimitivesContainer(); ArrayList primitives = container.primitives; primitives.add(Long.MIN_VALUE); primitives.add(Long.MAX_VALUE); primitives.add(Integer.MIN_VALUE); primitives.add(Integer.MAX_VALUE); primitives.add(Float.MIN_VALUE); primitives.add(Float.MAX_VALUE); primitives.add(Double.MIN_VALUE); primitives.add(Double.MAX_VALUE); String json = mObjectMapper.convertValue(container, JSONObject.class).toString(); JSONObject obj = new JSONObject(json); JSONArray array = obj.getJSONArray("primitives"); ArrayList actual = new ArrayList<>(); for (int i = 0, N = array.length(); i < N; i++) { actual.add(array.get(i)); } assertEquals(primitives.toString(), actual.toString()); } public static class ArrayOfPrimitivesContainer { @JsonProperty public final ArrayList primitives = new ArrayList<>(); } public static class NestedJsonProperty { @JsonProperty(required = true) public JsonPropertyString child1; @JsonProperty public JsonPropertyInt child2; @Override public boolean equals(Object o) { if (o == null || !(o instanceof NestedJsonProperty)) { return false; } return Objects.equals(child1, ((NestedJsonProperty) o).child1) && Objects.equals(child2, ((NestedJsonProperty) o).child2); } } public static class JsonPropertyString { @JsonProperty public String testString; @Override public boolean equals(Object o) { if (o == null || !(o instanceof JsonPropertyString)) { return false; } return Objects.equals(testString, ((JsonPropertyString) o).testString); } } public static class JsonPropertyInt { @JsonProperty public int i; @Override public boolean equals(Object o) { if (o == null || !(o instanceof JsonPropertyInt)) { return false; } return Objects.equals(i, ((JsonPropertyInt) o).i); } } public static class JsonPropertyEnum { @JsonProperty public TestEnum enumValue; @Override public boolean equals(Object o) { if (o == null || !(o instanceof JsonPropertyEnum)) { return false; } return Objects.equals(enumValue, ((JsonPropertyEnum) o).enumValue); } } public static class JsonPropertyStringList { @JsonProperty public List stringList; @Override public boolean equals(Object o) { if (o == null || !(o instanceof JsonPropertyStringList)) { return false; } JsonPropertyStringList rhs = (JsonPropertyStringList) o; if (stringList == null || rhs.stringList == null) { return stringList == rhs.stringList; } if (stringList.size() != rhs.stringList.size()) { return false; } ListIterator myIter = stringList.listIterator(); ListIterator rhsIter = rhs.stringList.listIterator(); while (myIter.hasNext()) { if (!Objects.equals(myIter.next(), rhsIter.next())) { return false; } } return true; } } public enum TestEnum { VALUE_ONE("one"), VALUE_TWO("two"), VALUE_THREE("three"); private final String mValue; private TestEnum(String str) { mValue = str; } @JsonValue public String getValue() { return mValue; } } private static class JsonPropertyMultitypedList { @JsonProperty public List multitypedList; } private static class JsonPropertyListOfLists { @JsonProperty public List> listOfLists; } } ================================================ FILE: stetho-js-rhino/.gitignore ================================================ /build ================================================ FILE: stetho-js-rhino/README.md ================================================ # Stetho's JavaScript Module This [Stetho](https://facebook.github.io/stetho) plugin adds a JavaScript console by embedding Mozilla's [Rhino](https://github.com/mozilla/rhino). ## Set-up ### Download Download [the latest JARs](https://github.com/facebook/stetho/releases/latest) or grab via Gradle: ```groovy implementation 'com.facebook.stetho:stetho-js-rhino:1.4.2' ``` or Maven: ```xml com.facebook.stetho stetho-js-rhino 1.4.2 ``` Make sure that you depend on the main `stetho` dependency too. ### Putting it together The Rhino JavaScript integration is automatically detected by Stetho and is enabled simply by adding the `stetho-js-rhino` dependency to your project. If you want to configure the JavaScript environment you can pass your own variables, classes, packages and functions and provide this custom runtime REPL using: ```java Stetho.initialize(Stetho.newInitializerBuilder(context) .enableWebKitInspector(new InspectorModulesProvider() { @Override public Iterable get() { return new DefaultInspectorModulesBuilder(context).runtimeRepl( new JsRuntimeReplFactoryBuilder(context) // Pass to JavaScript: var foo = "bar"; .addVariable("foo", "bar") .build() ).finish(); } }) .build(); ``` For more details see the next sections. ### How it works At the core this plugin initializes a JavaScript runtime provided by Mozilla's [Rhino](https://github.com/mozilla/rhino). The runtime is configured so that it will work within an Android application. This means that code has to run in interpreted mode as the more aggressive optimizations performed by Rhino are done through on-the-fly JVM bytecode generation and this won't work in Android which expects Dalvik bytecode. For generic purposes the interpreted mode should have no performance impact since this is debug tool. ## Customization By default a JavaScript interpreter starts with an empty scope (environment) and has no default variables or functions set besides the ones described by the language specification. You might be used to the browser setting up various objects for your like `document`, `console`, etc. This is something that's not part of the javascript specification and is particular only to the browser's runtime. ## Example Once you have enabled the JavaScript console in your app you will be able to run live JavaScript commands on your app from the console. Here's an example to show a toast from the console: ```javascript importPackage(android.widget); importPackage(android.os); var handler = new Handler(Looper.getMainLooper()); handler.post(function() { Toast.makeText(context, "Hello from JavaScript", Toast.LENGTH_LONG).show() }); ``` ### Default scope Rhino offers the possibility to use an enhanced runtime where some utilities have been added in order to help the integration with the java runtime. This is exactly the runtime that the plugin uses. The default scope used by the plugin is described in more details in the article [Scripting Java](https://developer.mozilla.org/en-US/docs/Mozilla/Projects/Rhino/Scripting_Java). The scope can be enhanced by your application, if desired. You can preload classes and packages, bind variables, objects and even functions. This means that your java classes and objects can be accessed from JavaScript. ### Builtins The JavaScript runtime use by this plugin has been enhanced. By default your application's package has been imported. So things like `R.string.app` should work. The functions `importClass` and `importPackage` have been added. A `console` object is available too. It supports only a `log()` method for now. ### Import a class First define a JsRuntimeReplFactoryBuilder object: ``` // context is your application context JsRuntimeReplFactoryBuilder jsRuntimeBuilder = new JsRuntimeReplFactoryBuilder(context); ``` To import a java class into the JavaScript runtime do: ```java jsRuntimeBuilder.importClass(R.class); ``` ### Import a package To import all classes in a java package into the JavaScript runtime do: ```java jsRuntimeBuilder.importPackage("android.content"); ``` ### Variable binding Here's how to pass a variable to the JavaScript runtime: ```java jsRuntimeBuilder.addVariable("flag", new AtomicBoolean(true)); ``` **Note**: Java primitive types will be autoboxed, only objects can be passed to the javascript runtime. ### Function binding You can also add custom javascript functions that will be available to the runtime. This requires a bit more of work on your part. Remember that you invoke methods on objects that you have already binded. If you really want to define a top level function this is how it can be done: ```java // Your application context final Context context = this; final Handler handler = new Handler(Looper.getMainLooper()); // Add the function: void toast(String) jsRuntimeBuilder.addFunction("toast", new BaseFunction() { @Override public Object call(org.mozilla.javascript.Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { // javascript passes the arguments as varags final String message = args[0].toString(); handler.post(new Runnable() { @Override public void run() { Toast.makeText(context, message, Toast.LENGTH_LONG).show(); } }); // return undef in javascript return org.mozilla.javascript.Context.getUndefinedValue(); } }); ``` **Note**: This is a complex example since a toast be invoked from the main UI thread. ## Limitations As mentioned in the section [how it works](#how-it-works) the JavaScript runtime has to run in interpreted mode due to the nature of the android runtime. ### Dex method count Rhino is not a small library and it can increase your dex method count by more than 7,000. The standard Rhino distribution includes a "tools" package that's not required by this plugin but it is still bundled. That package alone adds more than 1,200 methods to the dex count. Hopefully the Rhino devs will split the distribution in smaller artifacts (see https://github.com/mozilla/rhino/issues/156). In the meanwhile you should consider using proguard to shrink the dex method count. Here's the dex count of the stetho-sample compiled under various scenarios: | | Original | JavaScript | | :--- | -------: | ---------: | | Dex | 15,461 | 22,749 | | Size | 1.0M | 1.6M | With proguard: | | Original | JavaScript | JavaScript (w/o *tools*) | | :--- | -------: | ---------: | ---------------------------: | | Dex | 8,013 | 15,316 | 14,043 | | Size | 847K | 1.5M | 1.4M | ### Proguard To proguard your project add the following rules to your proguard file: ``` # stetho +keep class com.facebook.stetho.** { *; } # rhino (javascript) -dontwarn org.mozilla.javascript.** -dontwarn org.mozilla.classfile.** -keep class org.mozilla.javascript.** { *; } ``` If you want to remove the *tools* package for a more aggressive proguard use: ``` # stetho +keep class com.facebook.stetho.** { *; } # rhino (javascript) -dontwarn org.mozilla.javascript.** -dontwarn org.mozilla.classfile.** -keep class org.mozilla.classfile.** { *; } -keep class org.mozilla.javascript.* { *; } -keep class org.mozilla.javascript.annotations.** { *; } -keep class org.mozilla.javascript.ast.** { *; } -keep class org.mozilla.javascript.commonjs.module.** { *; } -keep class org.mozilla.javascript.commonjs.module.provider.** { *; } -keep class org.mozilla.javascript.debug.** { *; } -keep class org.mozilla.javascript.jdk13.** { *; } -keep class org.mozilla.javascript.jdk15.** { *; } -keep class org.mozilla.javascript.json.** { *; } -keep class org.mozilla.javascript.optimizer.** { *; } -keep class org.mozilla.javascript.regexp.** { *; } -keep class org.mozilla.javascript.serialize.** { *; } -keep class org.mozilla.javascript.typedarrays.** { *; } -keep class org.mozilla.javascript.v8dtoa.** { *; } -keep class org.mozilla.javascript.xml.** { *; } -keep class org.mozilla.javascript.xmlimpl.** { *; } ``` ================================================ FILE: stetho-js-rhino/build.gradle ================================================ apply plugin: 'com.android.library' android { compileSdkVersion rootProject.ext.compileSdkVersion defaultConfig { minSdkVersion 14 targetSdkVersion rootProject.ext.targetSdkVersion versionCode 1 versionName "1.0" consumerProguardFiles 'proguard-consumer.pro' } lintOptions { // Rhino has references to awt and swing disable 'InvalidPackage' } } dependencies { implementation project(':stetho') implementation 'com.google.code.findbugs:jsr305:3.0.2' implementation 'org.mozilla:rhino:1.7.6' implementation 'androidx.annotation:annotation:1.1.0' testImplementation 'junit:junit:4.12' } apply from: rootProject.file('release.gradle') ================================================ FILE: stetho-js-rhino/gradle.properties ================================================ POM_NAME=Stetho JavaScript (Rhino) module POM_ARTIFACT_ID=stetho-js-rhino POM_OPTIONAL_DEPS=com.facebook.stetho:stetho POM_PACKAGING=aar ================================================ FILE: stetho-js-rhino/proguard-consumer.pro ================================================ -keep class com.facebook.stetho.rhino.** { *; } # rhino (javascript) -dontwarn org.mozilla.javascript.** -dontwarn org.mozilla.classfile.** -keep class org.mozilla.classfile.** { *; } -keep class org.mozilla.javascript.* { *; } -keep class org.mozilla.javascript.annotations.** { *; } -keep class org.mozilla.javascript.ast.** { *; } -keep class org.mozilla.javascript.commonjs.module.** { *; } -keep class org.mozilla.javascript.commonjs.module.provider.** { *; } -keep class org.mozilla.javascript.debug.** { *; } -keep class org.mozilla.javascript.jdk13.** { *; } -keep class org.mozilla.javascript.jdk15.** { *; } -keep class org.mozilla.javascript.json.** { *; } -keep class org.mozilla.javascript.optimizer.** { *; } -keep class org.mozilla.javascript.regexp.** { *; } -keep class org.mozilla.javascript.serialize.** { *; } -keep class org.mozilla.javascript.typedarrays.** { *; } -keep class org.mozilla.javascript.v8dtoa.** { *; } -keep class org.mozilla.javascript.xml.** { *; } -keep class org.mozilla.javascript.xmlimpl.** { *; } ================================================ FILE: stetho-js-rhino/src/main/AndroidManifest.xml ================================================ ================================================ FILE: stetho-js-rhino/src/main/java/com/facebook/stetho/rhino/JsConsole.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.rhino; import com.facebook.stetho.inspector.console.CLog; import com.facebook.stetho.inspector.protocol.module.Console.MessageLevel; import com.facebook.stetho.inspector.protocol.module.Console.MessageSource; import org.mozilla.javascript.Context; import org.mozilla.javascript.Function; import org.mozilla.javascript.ScriptRuntime; import org.mozilla.javascript.Scriptable; import org.mozilla.javascript.ScriptableObject; import org.mozilla.javascript.annotations.JSFunction; public class JsConsole extends ScriptableObject { /** * Serial version UID. */ private static final long serialVersionUID = 1L; /** *

The zero-parameter constructor.

* *

When Context.defineClass is called with this class, it will construct * JsConsole.prototype using this constructor.

*/ public JsConsole() { // Empty } public JsConsole(ScriptableObject scope) { setParentScope(scope); Object ctor = ScriptRuntime.getTopLevelProp(scope, "Console"); if (ctor != null && ctor instanceof Scriptable) { Scriptable scriptable = (Scriptable) ctor; setPrototype((Scriptable) scriptable.get("prototype", scriptable)); } } @Override public String getClassName() { return "Console"; } @JSFunction public static void log(Context cx, Scriptable thisObj, Object[] args, Function funObj) { log(args); } // See https://developer.chrome.com/devtools/docs/console-api#consolelogobject-object private static void log(Object [] rawArgs) { String message = JsFormat.parse(rawArgs); CLog.writeToConsole(MessageLevel.LOG, MessageSource.JAVASCRIPT, message); } } ================================================ FILE: stetho-js-rhino/src/main/java/com/facebook/stetho/rhino/JsFormat.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.rhino; import java.util.Locale; import java.util.regex.Matcher; import java.util.regex.Pattern; import androidx.annotation.NonNull; /** *

Formatter that tries to mimic

console.log()
's format as close as possible.

* *

We have to use a custom formatter because

String.format()
will fail with JavaScript * numbers. Using the conversion format %d in JavaScript causes a problem with Java's
String.format()
. * This happens because %d expects an int/Integer but in JavaScript numbers are floats! *

* *

See Console API.

*/ class JsFormat { /** * Format specifier pattern. *

* %[argument_index$][flags][width][.precision]conversion */ private static final Pattern FORMAT_SPECIFIER_PATTERN = Pattern.compile( "^%" + "([0-9]+ [$])?" // Index + "([0-9]+)?" // Width + "([.] [0-9]+)?" // Precision + "([difs])", // Conversion Pattern.COMMENTS ); /** * Simple wrapper around a char[]. New versions of java make a full copy of * the string when doing substring(). With this class we avoid the copies * and substring is still O(1) vs O(n). */ private static class ArrayCharSequence implements CharSequence { private final @NonNull char[] array; private final int start; private final int end; private ArrayCharSequence(@NonNull char[] array, int start, int end) { this.array = array; this.start = start; this.end = end; } @Override public int length() { return end - start; } @Override public char charAt(int index) { return array[start + index]; } @NonNull @Override public CharSequence subSequence(int start, int end) { return new ArrayCharSequence(array, this.start + start, this.start + end); } private @NonNull CharSequence substring(int start) { return new ArrayCharSequence(array, this.start + start, this.start + end); } @Override public @NonNull String toString() { return new String(array, start, end - start); } } /** * Takes the arguments that console.log() would use, parses them and returns the * final string to output. * * @param args format and arguments * @return a string with the message to output */ static @NonNull String parse(@NonNull Object...args) { // The params available, we need to know if they where taken or not boolean[] argsUsed = new boolean[args.length]; // The first argument is the format and is always used String format = (String) args[0]; argsUsed[0] = true; int nextArgIndex = 1; // Scan the format and find all %s patterns final char[] chars = format.toCharArray(); StringBuilder buffer = new StringBuilder(); ArrayCharSequence sequence = new ArrayCharSequence(chars, 0, chars.length); for (int i = 0; i < chars.length; ++i) { char c = chars[i]; if (c != '%') { // Keep eating chars until we get a '%' buffer.append(c); continue; } // Found a %, is it a stand alone one? Matcher matcher = FORMAT_SPECIFIER_PATTERN.matcher(sequence.substring(i)); if (!matcher.find()) { // Didn't find a valid format specifier, maybe it is a "%%" ? if (i + 1 < chars.length) { char peek = chars[i + 1]; if (peek == '%') { // Found "%%" which at the end maps as a single '%' ++i; } } // A stand alone '%', we will just print it as it is buffer.append('%'); continue; } // Analyze the format. We don't have named captures in android yet so we will inspect // the groups. They are each optional but we can find out which one is which easily. // Remember that we want to parse: %[argument_index$][flags][width][.precision]conversion // // - `index` ends with '$' // - `precision` starts with '.' // - we don't support flags // - `width` is just numbers // - `conversion` is a single letter int groupCount = matcher.groupCount(); int index = -1; int width = -1; int precision = -1; char conversion = 0; for (int groupIdx = 1; groupIdx <= groupCount; ++groupIdx) { String value = matcher.group(groupIdx); if (value == null || value.equals("")) { // Empty group, we ignore it continue; } if (value.endsWith("$")) { // The `index` (it ends with a '$') value = value.substring(0, value.length() - 1); index = Integer.parseInt(value); continue; } char first = value.charAt(0); if (first == '.') { // The `precision` (it starts with a dot '.') value = value.substring(1); precision = Integer.parseInt(value); } else if (first >= '0' && first <= '9') { // The `width` width = Integer.parseInt(value); } else { // It has to be the `conversion` conversion = first; } } // Now we try to see which argument we have to take String currentFormat = matcher.group(); final Object value; final boolean found; if (index > argsUsed.length || (width > -1 && index == -1)) { // Index out of bounds (%1234$d), print the format as it is and ignore value = null; found = false; } else if (index <= argsUsed.length && index > 0) { // Index if valid (%3$d) value = args[index]; argsUsed[index] = true; nextArgIndex = index + 1; found = true; } else { // No index provided (%d) if (nextArgIndex < argsUsed.length) { value = args[nextArgIndex]; argsUsed[nextArgIndex] = true; ++nextArgIndex; found = true; } else { // We have way too many %d, more than we have arguments! value = null; found = false; } } if (!found) { // Just dump the placeholder text as it is and ignore buffer.append(currentFormat); i += currentFormat.length() - 1; continue; } // Apply the conversion switch (conversion) { case 'd': case 'i': Object l; if (value instanceof String) { try { l = Long.parseLong((String) value); } catch (NumberFormatException e) { l = "NaN"; } } else if (value instanceof Number) { l = ((Number) value).intValue(); } else { l = 0; } buffer.append(l); break; case 'f': Object d; if (value instanceof String) { try { d = Double.parseDouble((String) value); } catch (NumberFormatException e) { d = "NaN"; } } else if (value instanceof Number) { d = ((Number) value).doubleValue(); } else { d = 0; } if (precision > -1 && d instanceof Number) { d = String.format(Locale.US, "%." + precision + "f", d); } buffer.append(d); break; case 's': default: buffer.append(value); break; } i += currentFormat.length() - 1; } // Concatenate all params that have not been used for (int j = 0; j < argsUsed.length; j++) { boolean argUsed = argsUsed[j]; if (!argUsed) { buffer.append(" "); buffer.append(args[j]); } } return buffer.toString(); } } ================================================ FILE: stetho-js-rhino/src/main/java/com/facebook/stetho/rhino/JsRuntimeRepl.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.rhino; import com.facebook.stetho.inspector.console.RuntimeRepl; import org.mozilla.javascript.Context; import org.mozilla.javascript.ScriptableObject; import androidx.annotation.NonNull; import androidx.annotation.Nullable; class JsRuntimeRepl implements RuntimeRepl { private final @NonNull ScriptableObject mJsScope; JsRuntimeRepl(@NonNull ScriptableObject scope) { mJsScope = scope; } @Override public @Nullable Object evaluate(@NonNull String expression) throws Throwable { Object result; final Context jsContext = enterJsContext(); try { result = jsContext.evaluateString(mJsScope, expression, "chrome", 1, null); // Google chrome automatically saves the last expression to `$_`, we do the same Object jsValue = Context.javaToJS(result, mJsScope); ScriptableObject.putProperty(mJsScope, "$_", jsValue); } finally { Context.exit(); } return Context.jsToJava(result, Object.class); } /** * Setups a proper javascript context so that it can run javascript code properly under android. * For android we need to disable bytecode generation since the android vms don't understand JVM bytecode. * @return a proper javascript context */ static @NonNull Context enterJsContext() { final Context jsContext = Context.enter(); // If we cause the context to throw a runtime exception from this point // we need to make sure that exit the context. try { jsContext.setLanguageVersion(Context.VERSION_1_8); // We can't let Rhino to optimize the JS and to use a JIT because it would generate JVM bytecode // and android runs on DEX bytecode. Instead we need to go in interpreted mode. jsContext.setOptimizationLevel(-1); } catch (RuntimeException e) { // Something bad happened to the javascript context but it might still be usable. // The first thing to do is to exit the context and then propagate the error. Context.exit(); throw e; } return jsContext; } } ================================================ FILE: stetho-js-rhino/src/main/java/com/facebook/stetho/rhino/JsRuntimeReplFactoryBuilder.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.rhino; import android.util.Log; import com.facebook.stetho.common.LogUtil; import com.facebook.stetho.inspector.console.CLog; import com.facebook.stetho.inspector.console.RuntimeRepl; import com.facebook.stetho.inspector.console.RuntimeReplFactory; import com.facebook.stetho.inspector.protocol.module.Console; import org.mozilla.javascript.Context; import org.mozilla.javascript.Function; import org.mozilla.javascript.ImporterTopLevel; import org.mozilla.javascript.Scriptable; import org.mozilla.javascript.ScriptableObject; import org.mozilla.javascript.Undefined; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import androidx.annotation.NonNull; /** *

Builder used to setup the javascript runtime to be used by stetho.

* *

You can use this builder to configure the javacript environment by preloading: *

    *
  • Java classes
  • *
  • Java packages (with all their java classes)
  • *
  • Variables
  • *
  • Functions
  • *
*

* *

Your application context package is automatically visible with this builder.

*/ public class JsRuntimeReplFactoryBuilder { /** * Name of the "source" file used for reporting JavaScript compilation errors (or runtime errors). * Since this is evaluated from a chrome inspector window we pass "chrome". */ private static final String SOURCE_NAME = "chrome"; /** * Android application context. */ private final android.content.Context mContext; /** * Java classes to import into the javascript environment. */ private final Set> mClasses = new HashSet<>(); /** * Java packages to import into the javascript environment. * All classes inside the package will be imported. */ private final Set mPackages = new HashSet<>(); /** * Variables to bind to the javascript environment. */ private final Map mVariables = new HashMap<>(); /** * Global mFunctions to add to the javascript environment. */ private final Map mFunctions = new HashMap<>(); public static RuntimeReplFactory defaultFactory(@NonNull android.content.Context context) { return new JsRuntimeReplFactoryBuilder(context).build(); } public JsRuntimeReplFactoryBuilder(@NonNull android.content.Context context) { mContext = context; // We import the app's package name by default mPackages.add(context.getPackageName()); // Predefine $_ which holds the value of the last expression evaluated mVariables.put("$_", Context.getUndefinedValue()); } /** * Request that the given java class be imported in the javascript runtime. * @param aClass the java class to import * @return the builder */ public @NonNull JsRuntimeReplFactoryBuilder importClass(@NonNull Class aClass) { mClasses.add(aClass); return this; } /** * Request that the given package name will be imported in the javascript runtime. * This means that all classes (enums and interfaces) will be imported. * @param packageName the java package name to import * @return the builder */ public @NonNull JsRuntimeReplFactoryBuilder importPackage(@NonNull String packageName) { mPackages.add(packageName); return this; } /** * Add a variable (binding) to the javascript environment. * @param name the javascript variable name * @param value the value to add * @return the builder */ public JsRuntimeReplFactoryBuilder addVariable(@NonNull String name, Object value) { mVariables.put(name, value); return this; } /** * Adds a function to the javascript environment. * @param name the javascript function name * @param function the function * @return the builder */ public @NonNull JsRuntimeReplFactoryBuilder addFunction(@NonNull String name, @NonNull Function function) { mFunctions.put(name, function); return this; } /** * Build the runtime REPL instance to be supplied to the Stetho {@code Runtime} module. */ public RuntimeReplFactory build() { return new RuntimeReplFactory() { @Override public RuntimeRepl newInstance() { return new JsRuntimeRepl(initJsScope()); } }; } /** * Initializes a proper javascript scope (runtime environment holding variables). * @return a javascript scope */ private @NonNull ScriptableObject initJsScope() { final Context jsContext = JsRuntimeRepl.enterJsContext(); try { ScriptableObject scope = initJsScope(jsContext); return scope; } finally { Context.exit(); } } private @NonNull ScriptableObject initJsScope(@NonNull Context jsContext) { // Set the main Rhino goodies ImporterTopLevel importerTopLevel = new ImporterTopLevel(jsContext); ScriptableObject scope = jsContext.initStandardObjects(importerTopLevel, false); ScriptableObject.putProperty(scope, "context", Context.javaToJS(mContext, scope)); try { importClasses(jsContext, scope); importPackages(jsContext, scope); importConsole(scope); importVariables(scope); importFunctions(scope); } catch (StethoJsException e) { String message = String.format("%s\n%s", e.getMessage(), Log.getStackTraceString(e)); LogUtil.e(e, message); CLog.writeToConsole(Console.MessageLevel.ERROR, Console.MessageSource.JAVASCRIPT, message); } return scope; } private void importClasses(@NonNull Context jsContext, @NonNull ScriptableObject scope) throws StethoJsException { // Import the classes that the caller requested for (Class aClass : mClasses) { String className = aClass.getName(); try { // import from default classes String expression = String.format("importClass(%s)", className); jsContext.evaluateString(scope, expression, SOURCE_NAME, 1, null); } catch (Exception e) { try { // import from application classes String expression = String.format("importClass(Packages.%s)", className); jsContext.evaluateString(scope, expression, SOURCE_NAME, 1, null); } catch (Exception e1) { throw new StethoJsException(e1, "Failed to import class: %s", className); } } } } private void importPackages(@NonNull Context jsContext, @NonNull ScriptableObject scope) throws StethoJsException { // Import the packages that the caller requested for (String packageName : mPackages) { try { // import from default packages String expression = String.format("importPackage(%s)", packageName); jsContext.evaluateString(scope, expression, SOURCE_NAME, 1, null); } catch (Exception e) { try { // import from application packages String expression = String.format("importPackage(Packages.%s)", packageName); jsContext.evaluateString(scope, expression, SOURCE_NAME, 1, null); } catch (Exception e1) { throw new StethoJsException(e, "Failed to import package: %s", packageName); } } } } private void importConsole(@NonNull ScriptableObject scope) throws StethoJsException { // Set the `console` object try { ScriptableObject.defineClass(scope, JsConsole.class); JsConsole console = new JsConsole(scope); scope.defineProperty("console", console, ScriptableObject.DONTENUM); } catch (Exception e) { throw new StethoJsException(e, "Failed to setup javascript console"); } } private void importVariables(@NonNull ScriptableObject scope) throws StethoJsException { // Define the variables for (Map.Entry entrySet : mVariables.entrySet()) { String varName = entrySet.getKey(); Object varValue = entrySet.getValue(); try { Object jsValue; if (varValue instanceof Scriptable || varValue instanceof Undefined) { jsValue = varValue; } else { jsValue = Context.javaToJS(varValue, scope); } ScriptableObject.putProperty(scope, varName, jsValue); } catch (Exception e) { throw new StethoJsException(e, "Failed to setup variable: %s", varName); } } } private void importFunctions(@NonNull ScriptableObject scope) throws StethoJsException { // Define the functions for (Map.Entry entrySet : mFunctions.entrySet()) { String functionName = entrySet.getKey(); Function function = entrySet.getValue(); try { ScriptableObject.putProperty(scope, functionName, function); } catch (Exception e) { throw new StethoJsException(e, "Failed to setup function: %s", functionName); } } } private static class StethoJsException extends Exception { StethoJsException(Throwable rootCause, String format, Object...args) { super(args.length == 0 ? format : String.format(format, args), rootCause); } } } ================================================ FILE: stetho-js-rhino/src/test/java/com/facebook/stetho/rhino/JsFormatTest.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.rhino; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import static org.junit.Assert.assertEquals; @RunWith(JUnit4.class) public class JsFormatTest { @Test public void testParse() { assertEquals("%d", JsFormat.parse("%d")); assertEquals("23", JsFormat.parse("%d", 23)); assertEquals("2", JsFormat.parse("%d", 2.7)); assertEquals("23.37", JsFormat.parse("%.2f", 23.3678)); assertEquals("hellow world", JsFormat.parse("hellow %s", "world")); assertEquals("%$2d dp 5 1 2 3 4", JsFormat.parse("%$2d dp 5", 1, 2, 3 ,4)); assertEquals("2 dp 4 1 3 4", JsFormat.parse("%2$d dp 4", 1, 2, 3 ,4)); assertEquals("NaN dp 4 one three four", JsFormat.parse("%2$d dp 4", "one", "two", "three", "four")); assertEquals("two dp 4 one three four", JsFormat.parse("%2$s dp 4", "one", "two", "three", "four")); assertEquals("two vs two one three four", JsFormat.parse("%2$s vs %2$s", "one", "two", "three", "four")); assertEquals("two vs two one three four", JsFormat.parse("%2$s vs %2$s", "one", "two", "three", "four")); assertEquals("two vs four one three", JsFormat.parse("%2$s vs %4$s", "one", "two", "three", "four")); assertEquals("two vs four %.3$4f one three 1234.5678", JsFormat.parse("%2$s vs %4$s %.3$4f", "one", "two", "three", "four", 1234.5678)); assertEquals("two vs four %.2$4f one three 1234.5678", JsFormat.parse("%2$s vs %4$s %.2$4f", "one", "two", "three", "four", 1234.5678)); assertEquals("two vs four 1234.57 one three", JsFormat.parse("%2$s vs %4$s %.2f", "one", "two", "three", "four", 1234.5678)); assertEquals("1234.57", JsFormat.parse("%.2f", 1234.5678)); assertEquals("%,2f 1234.5678", JsFormat.parse("%,2f", 1234.5678)); assertEquals("1234.57", JsFormat.parse("%.2f", 1234.5678)); assertEquals("NaN", JsFormat.parse("%.2f", "hello")); assertEquals("%.2a hello", JsFormat.parse("%.2a", "hello")); assertEquals("% cool .2a hello", JsFormat.parse("%% cool .2a", "hello")); assertEquals("two vs four 1234.5678 one three", JsFormat.parse("%2$s vs %4$s %s", "one", "two", "three", "four", 1234.5678)); assertEquals("two vs four %s", JsFormat.parse("two vs four %s")); assertEquals("two vs four % one three", JsFormat.parse("two vs four %", "one", "three")); assertEquals("two one two four oops three", JsFormat.parse("%2$s %1$s %s %4$s %s", "one", "two", "three", "four", "oops")); } } ================================================ FILE: stetho-okhttp/.gitignore ================================================ /build ================================================ FILE: stetho-okhttp/build.gradle ================================================ apply plugin: 'com.android.library' android { compileSdkVersion rootProject.ext.compileSdkVersion defaultConfig { minSdkVersion 14 targetSdkVersion rootProject.ext.targetSdkVersion 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 { implementation project(':stetho') implementation 'com.google.code.findbugs:jsr305:3.0.2' implementation 'com.squareup.okhttp:okhttp:2.7.2' testImplementation 'junit:junit:4.12' testImplementation('org.robolectric:robolectric:2.4') { exclude module: 'commons-logging' exclude module: 'httpclient' } testImplementation 'org.powermock:powermock-api-mockito:1.6.6' testImplementation 'org.powermock:powermock-module-junit4:1.6.6' // Needed for Robolectric and PowerMock to be combined in a single test. testImplementation 'org.powermock:powermock-module-junit4-rule:1.6.6' testImplementation 'org.powermock:powermock-classloading-xstream:1.6.6' testImplementation 'com.squareup.okhttp:mockwebserver:2.7.2' } apply from: rootProject.file('release.gradle') ================================================ FILE: stetho-okhttp/gradle.properties ================================================ POM_NAME=Stetho OkHttp module POM_ARTIFACT_ID=stetho-okhttp POM_PACKAGING=aar ================================================ FILE: stetho-okhttp/src/main/AndroidManifest.xml ================================================ ================================================ FILE: stetho-okhttp/src/main/java/com/facebook/stetho/okhttp/StethoInterceptor.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.okhttp; import com.facebook.stetho.inspector.network.DefaultResponseHandler; import com.facebook.stetho.inspector.network.NetworkEventReporter; import com.facebook.stetho.inspector.network.NetworkEventReporterImpl; import com.facebook.stetho.inspector.network.RequestBodyHelper; 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; import javax.annotation.Nullable; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; /** * Provides easy integration with OkHttp 2.2.0+ * by way of the new Interceptor * system. To use: *
 *   OkHttpClient client = new OkHttpClient();
 *   client.networkInterceptors().add(new StethoInterceptor());
 * 
* * @deprecated replaced with {@code com.facebook.stetho.okhttp3.StethoInterceptor}. */ @Deprecated public class StethoInterceptor implements Interceptor { private final NetworkEventReporter mEventReporter = NetworkEventReporterImpl.get(); @Override public Response intercept(Chain chain) throws IOException { String requestId = mEventReporter.nextRequestId(); Request request = chain.request(); RequestBodyHelper requestBodyHelper = null; if (mEventReporter.isEnabled()) { requestBodyHelper = new RequestBodyHelper(mEventReporter, requestId); OkHttpInspectorRequest inspectorRequest = new OkHttpInspectorRequest(requestId, request, requestBodyHelper); mEventReporter.requestWillBeSent(inspectorRequest); } Response response; try { response = chain.proceed(request); } catch (IOException e) { if (mEventReporter.isEnabled()) { mEventReporter.httpExchangeFailed(requestId, e.toString()); } throw e; } if (mEventReporter.isEnabled()) { if (requestBodyHelper != null && requestBodyHelper.hasBody()) { requestBodyHelper.reportDataSent(); } Connection connection = chain.connection(); if (connection == null) { throw new IllegalStateException( "No connection associated with this request; " + "did you use addInterceptor instead of addNetworkInterceptor?"); } 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, response.header("Content-Encoding"), 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 RequestBodyHelper mRequestBodyHelper; public OkHttpInspectorRequest( String requestId, Request request, RequestBodyHelper requestBodyHelper) { mRequestId = requestId; mRequest = request; mRequestBodyHelper = requestBodyHelper; } @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 { RequestBody body = mRequest.body(); if (body == null) { return null; } OutputStream out = mRequestBodyHelper.createBodySink(firstHeaderValue("Content-Encoding")); BufferedSink bufferedSink = Okio.buffer(Okio.sink(out)); try { body.writeTo(bufferedSink); } finally { bufferedSink.close(); } return mRequestBodyHelper.getDisplayBody(); } @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() throws IOException { 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; } } } ================================================ FILE: stetho-okhttp/src/test/java/com/facebook/stetho/okhttp/StethoInterceptorTest.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.okhttp; import android.net.Uri; import android.os.Build; import com.facebook.stetho.inspector.network.DecompressionHelper; import com.facebook.stetho.inspector.network.NetworkEventReporter; import com.facebook.stetho.inspector.network.NetworkEventReporterImpl; import com.facebook.stetho.inspector.network.ResponseHandler; import com.squareup.okhttp.Connection; import com.squareup.okhttp.Interceptor; import com.squareup.okhttp.MediaType; import com.squareup.okhttp.OkHttpClient; import com.squareup.okhttp.Protocol; import com.squareup.okhttp.Request; import com.squareup.okhttp.RequestBody; import com.squareup.okhttp.Response; import com.squareup.okhttp.ResponseBody; import com.squareup.okhttp.mockwebserver.MockResponse; import com.squareup.okhttp.mockwebserver.MockWebServer; import okio.Buffer; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InOrder; import org.mockito.Mockito; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.powermock.api.mockito.PowerMockito; import org.powermock.core.classloader.annotations.PowerMockIgnore; import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.rule.PowerMockRule; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; import javax.annotation.Nullable; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.concurrent.atomic.AtomicReference; import java.util.zip.GZIPOutputStream; import static org.junit.Assert.*; import static org.mockito.Matchers.*; import static org.mockito.Mockito.mock; @Config(emulateSdk = Build.VERSION_CODES.JELLY_BEAN) @RunWith(RobolectricTestRunner.class) @PowerMockIgnore({ "org.mockito.*", "org.robolectric.*", "android.*", "javax.net.ssl.*" }) @PrepareForTest(NetworkEventReporterImpl.class) public class StethoInterceptorTest { @Rule public PowerMockRule rule = new PowerMockRule(); private NetworkEventReporter mMockEventReporter; private StethoInterceptor mInterceptor; private OkHttpClient mClientWithInterceptor; @Before public void setUp() { PowerMockito.mockStatic(NetworkEventReporterImpl.class); mMockEventReporter = mock(NetworkEventReporter.class); Mockito.when(mMockEventReporter.isEnabled()).thenReturn(true); PowerMockito.when(NetworkEventReporterImpl.get()).thenReturn(mMockEventReporter); mInterceptor = new StethoInterceptor(); mClientWithInterceptor = new OkHttpClient(); mClientWithInterceptor.networkInterceptors().add(mInterceptor); } @Test public void testHappyPath() throws IOException { InOrder inOrder = Mockito.inOrder(mMockEventReporter); hookAlmostRealRequestWillBeSent(mMockEventReporter); ByteArrayOutputStream capturedOutput = hookAlmostRealInterpretResponseStream(mMockEventReporter); Uri requestUri = Uri.parse("http://www.facebook.com/nowhere"); String requestText = "Test input"; Request request = new Request.Builder() .url(requestUri.toString()) .method( "POST", RequestBody.create(MediaType.parse("text/plain"), requestText)) .build(); String originalBodyData = "Success!"; Response reply = new Response.Builder() .request(request) .protocol(Protocol.HTTP_1_1) .code(200) .body(ResponseBody.create(MediaType.parse("text/plain"), originalBodyData)) .build(); Response filteredResponse = mInterceptor.intercept( new SimpleTestChain(request, reply, mock(Connection.class))); inOrder.verify(mMockEventReporter).isEnabled(); inOrder.verify(mMockEventReporter) .requestWillBeSent(any(NetworkEventReporter.InspectorRequest.class)); inOrder.verify(mMockEventReporter) .dataSent( anyString(), eq(requestText.length()), eq(requestText.length())); inOrder.verify(mMockEventReporter) .responseHeadersReceived(any(NetworkEventReporter.InspectorResponse.class)); String filteredResponseString = filteredResponse.body().string(); String interceptedOutput = capturedOutput.toString(); inOrder.verify(mMockEventReporter).dataReceived(anyString(), anyInt(), anyInt()); inOrder.verify(mMockEventReporter).responseReadFinished(anyString()); assertEquals(originalBodyData, filteredResponseString); assertEquals(originalBodyData, interceptedOutput); inOrder.verifyNoMoreInteractions(); } @Test public void testWithRequestCompression() throws IOException { AtomicReference capturedRequest = hookAlmostRealRequestWillBeSent(mMockEventReporter); MockWebServer server = new MockWebServer(); server.start(); server.enqueue(new MockResponse() .setBody("Success!")); final byte[] decompressed = "Request text".getBytes(); final byte[] compressed = compress(decompressed); assertNotEquals( "Bogus test: decompressed and compressed lengths match", compressed.length, decompressed.length); RequestBody compressedBody = RequestBody.create( MediaType.parse("text/plain"), compress(decompressed)); Request request = new Request.Builder() .url(server.getUrl("/")) .addHeader("Content-Encoding", "gzip") .post(compressedBody) .build(); Response response = mClientWithInterceptor.newCall(request).execute(); // Force a read to complete the flow. response.body().string(); assertArrayEquals(decompressed, capturedRequest.get().body()); Mockito.verify(mMockEventReporter) .dataSent( anyString(), eq(decompressed.length), eq(compressed.length)); server.shutdown(); } @Test public void testWithResponseCompression() throws IOException { ByteArrayOutputStream capturedOutput = hookAlmostRealInterpretResponseStream(mMockEventReporter); byte[] uncompressedData = repeat(".", 1024).getBytes(); byte[] compressedData = compress(uncompressedData); MockWebServer server = new MockWebServer(); server.start(); server.enqueue(new MockResponse() .setBody(new Buffer().write(compressedData)) .addHeader("Content-Encoding: gzip")); Request request = new Request.Builder() .url(server.url("/")) .build(); Response response = mClientWithInterceptor.newCall(request).execute(); // Verify that the final output and the caller both saw the uncompressed stream. assertArrayEquals(uncompressedData, response.body().bytes()); assertArrayEquals(uncompressedData, capturedOutput.toByteArray()); // And verify that the StethoInterceptor was able to see both. Mockito.verify(mMockEventReporter) .dataReceived( anyString(), eq(compressedData.length), eq(uncompressedData.length)); server.shutdown(); } private static String repeat(String s, int reps) { StringBuilder b = new StringBuilder(s.length() * reps); while (reps-- > 0) { b.append(s); } return b.toString(); } private static byte[] compress(byte[] data) throws IOException { ByteArrayOutputStream buf = new ByteArrayOutputStream(); GZIPOutputStream out = new GZIPOutputStream(buf); out.write(data); out.close(); return buf.toByteArray(); } private static AtomicReference hookAlmostRealRequestWillBeSent( final NetworkEventReporter mockEventReporter) { final AtomicReference capturedRequest = new AtomicReference<>(null); Mockito.doAnswer( new Answer() { @Override public Void answer(InvocationOnMock invocation) throws Throwable { Object[] args = invocation.getArguments(); NetworkEventReporter.InspectorRequest request = (NetworkEventReporter.InspectorRequest)args[0]; capturedRequest.set(request); // Access the body, causing the body helper to perform decompression... request.body(); return null; } }) .when(mockEventReporter) .requestWillBeSent( any(NetworkEventReporter.InspectorRequest.class)); return capturedRequest; } /** * Provide a suitably "real" implementation of * {@link NetworkEventReporter#interpretResponseStream} for our mock to test that * events are properly delegated. */ private static ByteArrayOutputStream hookAlmostRealInterpretResponseStream( final NetworkEventReporter mockEventReporter) { final ByteArrayOutputStream capturedOutput = new ByteArrayOutputStream(); Mockito.when( mockEventReporter.interpretResponseStream( anyString(), anyString(), anyString(), any(InputStream.class), any(ResponseHandler.class))) .thenAnswer( new Answer() { @Override public InputStream answer(InvocationOnMock invocationOnMock) throws Throwable { Object[] args = invocationOnMock.getArguments(); String requestId = (String)args[0]; String contentEncoding = (String)args[2]; InputStream responseStream = (InputStream)args[3]; ResponseHandler responseHandler = (ResponseHandler)args[4]; return DecompressionHelper.teeInputWithDecompression( null /* networkPeerManager */, requestId, responseStream, capturedOutput, contentEncoding, responseHandler); } }); return capturedOutput; } private static class SimpleTestChain implements Interceptor.Chain { private final Request mRequest; private final Response mResponse; @Nullable private final Connection mConnection; public SimpleTestChain(Request request, Response response, @Nullable Connection connection) { mRequest = request; mResponse = response; mConnection = connection; } @Override public Request request() { return mRequest; } @Override public Response proceed(Request request) throws IOException { if (mRequest != request) { throw new IllegalArgumentException( "Expected " + System.identityHashCode(mRequest) + "; got " + System.identityHashCode(request)); } return mResponse; } @Override public Connection connection() { return mConnection; } } } ================================================ FILE: stetho-okhttp3/.gitignore ================================================ /build ================================================ FILE: stetho-okhttp3/build.gradle ================================================ apply plugin: 'com.android.library' android { compileSdkVersion rootProject.ext.compileSdkVersion defaultConfig { minSdkVersion 14 targetSdkVersion rootProject.ext.targetSdkVersion 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 { implementation project(':stetho') implementation 'com.google.code.findbugs:jsr305:3.0.2' implementation 'com.squareup.okhttp3:okhttp:3.4.2' testImplementation 'junit:junit:4.12' testImplementation('org.robolectric:robolectric:2.4') { exclude module: 'commons-logging' exclude module: 'httpclient' } testImplementation 'org.powermock:powermock-api-mockito:1.6.6' testImplementation 'org.powermock:powermock-module-junit4:1.6.6' // Needed for Robolectric and PowerMock to be combined in a single test. testImplementation 'org.powermock:powermock-module-junit4-rule:1.6.6' testImplementation 'org.powermock:powermock-classloading-xstream:1.6.6' testImplementation 'com.squareup.okhttp3:mockwebserver:3.4.2' } apply from: rootProject.file('release.gradle') ================================================ FILE: stetho-okhttp3/gradle.properties ================================================ POM_NAME=Stetho OkHttp 3 module POM_ARTIFACT_ID=stetho-okhttp3 POM_PACKAGING=aar ================================================ FILE: stetho-okhttp3/src/main/AndroidManifest.xml ================================================ ================================================ FILE: stetho-okhttp3/src/main/java/com/facebook/stetho/okhttp3/StethoInterceptor.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.okhttp3; import com.facebook.stetho.inspector.network.DefaultResponseHandler; import com.facebook.stetho.inspector.network.NetworkEventReporter; import com.facebook.stetho.inspector.network.NetworkEventReporterImpl; import com.facebook.stetho.inspector.network.RequestBodyHelper; import okhttp3.*; import okio.BufferedSink; import okio.BufferedSource; import okio.Okio; import javax.annotation.Nullable; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; /** * Provides easy integration with OkHttp 3.x by way of * the new Interceptor system. To * use: *
 *   OkHttpClient client = new OkHttpClient.Builder()
 *       .addNetworkInterceptor(new StethoInterceptor())
 *       .build();
 * 
*/ public class StethoInterceptor implements Interceptor { private final NetworkEventReporter mEventReporter = NetworkEventReporterImpl.get(); @Override public Response intercept(Chain chain) throws IOException { String requestId = mEventReporter.nextRequestId(); Request request = chain.request(); RequestBodyHelper requestBodyHelper = null; if (mEventReporter.isEnabled()) { requestBodyHelper = new RequestBodyHelper(mEventReporter, requestId); OkHttpInspectorRequest inspectorRequest = new OkHttpInspectorRequest(requestId, request, requestBodyHelper); mEventReporter.requestWillBeSent(inspectorRequest); } Response response; try { response = chain.proceed(request); } catch (IOException e) { if (mEventReporter.isEnabled()) { mEventReporter.httpExchangeFailed(requestId, e.toString()); } throw e; } if (mEventReporter.isEnabled()) { if (requestBodyHelper != null && requestBodyHelper.hasBody()) { requestBodyHelper.reportDataSent(); } Connection connection = chain.connection(); if (connection == null) { throw new IllegalStateException( "No connection associated with this request; " + "did you use addInterceptor instead of addNetworkInterceptor?"); } 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, response.header("Content-Encoding"), 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 RequestBodyHelper mRequestBodyHelper; public OkHttpInspectorRequest( String requestId, Request request, RequestBodyHelper requestBodyHelper) { mRequestId = requestId; mRequest = request; mRequestBodyHelper = requestBodyHelper; } @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.url().toString(); } @Override public String method() { return mRequest.method(); } @Nullable @Override public byte[] body() throws IOException { RequestBody body = mRequest.body(); if (body == null) { return null; } OutputStream out = mRequestBodyHelper.createBodySink(firstHeaderValue("Content-Encoding")); BufferedSink bufferedSink = Okio.buffer(Okio.sink(out)); try { body.writeTo(bufferedSink); } finally { bufferedSink.close(); } return mRequestBodyHelper.getDisplayBody(); } @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 @Nullable final Connection mConnection; public OkHttpInspectorResponse( String requestId, Request request, Response response, @Nullable Connection connection) { mRequestId = requestId; mRequest = request; mResponse = response; mConnection = connection; } @Override public String requestId() { return mRequestId; } @Override public String url() { return mRequest.url().toString(); } @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 == null ? 0 : 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; } } } ================================================ FILE: stetho-okhttp3/src/test/java/com/facebook/stetho/okhttp3/StethoInterceptorTest.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.okhttp3; import android.net.Uri; import android.os.Build; import com.facebook.stetho.inspector.network.DecompressionHelper; import com.facebook.stetho.inspector.network.NetworkEventReporter; import com.facebook.stetho.inspector.network.NetworkEventReporterImpl; import com.facebook.stetho.inspector.network.ResponseHandler; import okhttp3.Connection; import okhttp3.Interceptor; import okhttp3.MediaType; import okhttp3.OkHttpClient; import okhttp3.Protocol; import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; import okhttp3.ResponseBody; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import okio.Buffer; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InOrder; import org.mockito.Mockito; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.powermock.api.mockito.PowerMockito; import org.powermock.core.classloader.annotations.PowerMockIgnore; import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.rule.PowerMockRule; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; import javax.annotation.Nullable; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.concurrent.atomic.AtomicReference; import java.util.zip.GZIPOutputStream; import static org.junit.Assert.*; import static org.mockito.Matchers.*; import static org.mockito.Mockito.mock; @Config(emulateSdk = Build.VERSION_CODES.JELLY_BEAN) @RunWith(RobolectricTestRunner.class) @PowerMockIgnore({ "org.mockito.*", "org.robolectric.*", "android.*", "javax.net.ssl.*" }) @PrepareForTest(NetworkEventReporterImpl.class) public class StethoInterceptorTest { @Rule public PowerMockRule rule = new PowerMockRule(); private NetworkEventReporter mMockEventReporter; private StethoInterceptor mInterceptor; private OkHttpClient mClientWithInterceptor; @Before public void setUp() { PowerMockito.mockStatic(NetworkEventReporterImpl.class); mMockEventReporter = mock(NetworkEventReporter.class); Mockito.when(mMockEventReporter.isEnabled()).thenReturn(true); PowerMockito.when(NetworkEventReporterImpl.get()).thenReturn(mMockEventReporter); mInterceptor = new StethoInterceptor(); mClientWithInterceptor = new OkHttpClient.Builder() .addNetworkInterceptor(mInterceptor) .build(); } @Test public void testHappyPath() throws IOException { InOrder inOrder = Mockito.inOrder(mMockEventReporter); hookAlmostRealRequestWillBeSent(mMockEventReporter); ByteArrayOutputStream capturedOutput = hookAlmostRealInterpretResponseStream(mMockEventReporter); Uri requestUri = Uri.parse("http://www.facebook.com/nowhere"); String requestText = "Test input"; Request request = new Request.Builder() .url(requestUri.toString()) .method( "POST", RequestBody.create(MediaType.parse("text/plain"), requestText)) .build(); String originalBodyData = "Success!"; Response reply = new Response.Builder() .request(request) .protocol(Protocol.HTTP_1_1) .code(200) .body(ResponseBody.create(MediaType.parse("text/plain"), originalBodyData)) .build(); Response filteredResponse = mInterceptor.intercept( new SimpleTestChain(request, reply, mock(Connection.class))); inOrder.verify(mMockEventReporter).isEnabled(); inOrder.verify(mMockEventReporter) .requestWillBeSent(any(NetworkEventReporter.InspectorRequest.class)); inOrder.verify(mMockEventReporter) .dataSent( anyString(), eq(requestText.length()), eq(requestText.length())); inOrder.verify(mMockEventReporter) .responseHeadersReceived(any(NetworkEventReporter.InspectorResponse.class)); String filteredResponseString = filteredResponse.body().string(); String interceptedOutput = capturedOutput.toString(); inOrder.verify(mMockEventReporter).dataReceived(anyString(), anyInt(), anyInt()); inOrder.verify(mMockEventReporter).responseReadFinished(anyString()); assertEquals(originalBodyData, filteredResponseString); assertEquals(originalBodyData, interceptedOutput); inOrder.verifyNoMoreInteractions(); } @Test public void testWithRequestCompression() throws IOException { AtomicReference capturedRequest = hookAlmostRealRequestWillBeSent(mMockEventReporter); MockWebServer server = new MockWebServer(); server.start(); server.enqueue(new MockResponse() .setBody("Success!")); final byte[] decompressed = "Request text".getBytes(); final byte[] compressed = compress(decompressed); assertNotEquals( "Bogus test: decompressed and compressed lengths match", compressed.length, decompressed.length); RequestBody compressedBody = RequestBody.create( MediaType.parse("text/plain"), compress(decompressed)); Request request = new Request.Builder() .url(server.url("/")) .addHeader("Content-Encoding", "gzip") .post(compressedBody) .build(); Response response = mClientWithInterceptor.newCall(request).execute(); // Force a read to complete the flow. response.body().string(); assertArrayEquals(decompressed, capturedRequest.get().body()); Mockito.verify(mMockEventReporter) .dataSent( anyString(), eq(decompressed.length), eq(compressed.length)); server.shutdown(); } @Test public void testWithResponseCompression() throws IOException { ByteArrayOutputStream capturedOutput = hookAlmostRealInterpretResponseStream(mMockEventReporter); byte[] uncompressedData = repeat(".", 1024).getBytes(); byte[] compressedData = compress(uncompressedData); MockWebServer server = new MockWebServer(); server.start(); server.enqueue(new MockResponse() .setBody(new Buffer().write(compressedData)) .addHeader("Content-Encoding: gzip")); Request request = new Request.Builder() .url(server.url("/")) .build(); Response response = mClientWithInterceptor.newCall(request).execute(); // Verify that the final output and the caller both saw the uncompressed stream. assertArrayEquals(uncompressedData, response.body().bytes()); assertArrayEquals(uncompressedData, capturedOutput.toByteArray()); // And verify that the StethoInterceptor was able to see both. Mockito.verify(mMockEventReporter) .dataReceived( anyString(), eq(compressedData.length), eq(uncompressedData.length)); server.shutdown(); } private static String repeat(String s, int reps) { StringBuilder b = new StringBuilder(s.length() * reps); while (reps-- > 0) { b.append(s); } return b.toString(); } private static byte[] compress(byte[] data) throws IOException { ByteArrayOutputStream buf = new ByteArrayOutputStream(); GZIPOutputStream out = new GZIPOutputStream(buf); out.write(data); out.close(); return buf.toByteArray(); } private static AtomicReference hookAlmostRealRequestWillBeSent( final NetworkEventReporter mockEventReporter) { final AtomicReference capturedRequest = new AtomicReference<>(null); Mockito.doAnswer( new Answer() { @Override public Void answer(InvocationOnMock invocation) throws Throwable { Object[] args = invocation.getArguments(); NetworkEventReporter.InspectorRequest request = (NetworkEventReporter.InspectorRequest)args[0]; capturedRequest.set(request); // Access the body, causing the body helper to perform decompression... request.body(); return null; } }) .when(mockEventReporter) .requestWillBeSent( any(NetworkEventReporter.InspectorRequest.class)); return capturedRequest; } /** * Provide a suitably "real" implementation of * {@link NetworkEventReporter#interpretResponseStream} for our mock to test that * events are properly delegated. */ private static ByteArrayOutputStream hookAlmostRealInterpretResponseStream( final NetworkEventReporter mockEventReporter) { final ByteArrayOutputStream capturedOutput = new ByteArrayOutputStream(); Mockito.when( mockEventReporter.interpretResponseStream( anyString(), anyString(), anyString(), any(InputStream.class), any(ResponseHandler.class))) .thenAnswer( new Answer() { @Override public InputStream answer(InvocationOnMock invocationOnMock) throws Throwable { Object[] args = invocationOnMock.getArguments(); String requestId = (String)args[0]; String contentEncoding = (String)args[2]; InputStream responseStream = (InputStream)args[3]; ResponseHandler responseHandler = (ResponseHandler)args[4]; return DecompressionHelper.teeInputWithDecompression( null /* networkPeerManager */, requestId, responseStream, capturedOutput, contentEncoding, responseHandler); } }); return capturedOutput; } private static class SimpleTestChain implements Interceptor.Chain { private final Request mRequest; private final Response mResponse; @Nullable private final Connection mConnection; public SimpleTestChain(Request request, Response response, @Nullable Connection connection) { mRequest = request; mResponse = response; mConnection = connection; } @Override public Request request() { return mRequest; } @Override public Response proceed(Request request) throws IOException { if (mRequest != request) { throw new IllegalArgumentException( "Expected " + System.identityHashCode(mRequest) + "; got " + System.identityHashCode(request)); } return mResponse; } @Override public Connection connection() { return mConnection; } } } ================================================ FILE: stetho-sample/.gitignore ================================================ /build ================================================ FILE: stetho-sample/build.gradle ================================================ repositories { // See dependencies below. mavenLocal() } apply plugin: 'com.android.application' android { compileSdkVersion rootProject.ext.compileSdkVersion defaultConfig { applicationId "com.facebook.stetho.sample" minSdkVersion 14 targetSdkVersion rootProject.ext.targetSdkVersion versionCode 1 versionName "1.0" } buildTypes { // release target excludes Stetho for illustration purposes. // See dependencies for instructions on how Stetho developers // can test this. release { debuggable false signingConfig signingConfigs.debug } } } dependencies { implementation project(':stetho') implementation project(':stetho-urlconnection') // Uncomment if you wish to play with the Console evaluation // features of the Rhino JS implementation. Disabled by default // because it is a large JAR by comparison to the rest of // Stetho. //debugCompile project(':stetho-js-rhino') // We must use Maven dependency resolution to demonstrate our usage // of optional dependencies to include stetho-urlconnection but not // the full blown stetho. For most projects, of course the simpler // way to express this is: // // dependencies { // debugImplementation 'com.facebook.stetho:stetho:' // implementation 'com.facebook.stetho:stetho-urlconnection:' // } // // For Stetho developers, to verify locally that things are working // properly, you must install the latest version to ~/.m2/repository as // such: // // ./gradlew installArchives // // Then uncomment the Maven style dependency and comment the project one: //releaseImplementation "com.facebook.stetho:stetho-urlconnection:${VERSION_NAME}" releaseImplementation project(':stetho-urlconnection') implementation 'com.google.code.findbugs:jsr305:3.0.2' } ================================================ FILE: stetho-sample/src/debug/AndroidManifest.xml ================================================ ================================================ FILE: stetho-sample/src/debug/java/com/facebook/stetho/sample/APODDumperPlugin.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.sample; import android.content.ContentResolver; import android.database.Cursor; import com.facebook.stetho.dumpapp.ArgsHelper; import com.facebook.stetho.dumpapp.DumpException; import com.facebook.stetho.dumpapp.DumpUsageException; import com.facebook.stetho.dumpapp.DumperContext; import com.facebook.stetho.dumpapp.DumperPlugin; import java.io.PrintStream; import java.util.Iterator; public class APODDumperPlugin implements DumperPlugin { private static final String NAME = "apod"; private static final String CMD_LIST = "list"; private static final String CMD_CLEAR = "clear"; private static final String CMD_DELETE = "delete"; private static final String CMD_REFRESH = "refresh"; private final ContentResolver mContentResolver; private final APODRssFetcher mAPODRssFetcher; public APODDumperPlugin(ContentResolver contentResolver) { mContentResolver = contentResolver; mAPODRssFetcher = new APODRssFetcher(mContentResolver); } @Override public String getName() { return NAME; } @Override public void dump(DumperContext dumpContext) throws DumpException { PrintStream writer = dumpContext.getStdout(); Iterator argsIter = dumpContext.getArgsAsList().iterator(); String command = ArgsHelper.nextOptionalArg(argsIter, null); if (CMD_LIST.equalsIgnoreCase(command)) { doList(writer); } else if (CMD_DELETE.equalsIgnoreCase(command)) { doRemove(writer, argsIter); } else if (CMD_CLEAR.equalsIgnoreCase(command)) { doClear(writer); } else if (CMD_REFRESH.equalsIgnoreCase(command)) { doRefresh(writer); } else { usage(writer); if (command != null) { throw new DumpUsageException("Unknown command: " + command); } } } private void doList(PrintStream writer) { Cursor cursor = mContentResolver.query( APODContract.CONTENT_URI, null /* projection */, null /* selection */, null /* selectionArgs */, APODContract.Columns._ID); int count = 0; while (cursor.moveToNext()) { writer.println(String.format("Row #%d", count++)); for (int i = 0; i < cursor.getColumnCount(); ++i) { writer.println(String.format(" %s: %s", cursor.getColumnName(i), cursor.getString(i))); } } writer.println(); } private void doRemove(PrintStream writer, Iterator argsIter) throws DumpUsageException { String rowId = ArgsHelper.nextArg(argsIter, "Expected rowId"); delete(writer, APODContract.Columns._ID + "=?", new String[] {rowId}); } private void doClear(PrintStream writer) { delete(writer, null, null); } private void doRefresh(PrintStream writer) { mAPODRssFetcher.fetchAndStore(); writer.println("Submitted request to fetch new data"); } private void delete(PrintStream writer, String where, String[] args) { int result = mContentResolver.delete(APODContract.CONTENT_URI, where, args); writer.println("Removed " + result + " rows."); } private static void usage(PrintStream writer) { final String cmdName = "dumpapp " + NAME; final String usagePrefix = "Usage: " + cmdName + " "; writer.println(usagePrefix + " [command-options]"); writer.print(usagePrefix + CMD_LIST); writer.println(); writer.print(usagePrefix + CMD_CLEAR); writer.println(); writer.print(usagePrefix + CMD_DELETE + " "); writer.println(); writer.print(usagePrefix + CMD_REFRESH); writer.println(); } } ================================================ FILE: stetho-sample/src/debug/java/com/facebook/stetho/sample/HelloWorldDumperPlugin.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.sample; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.PrintStream; import java.util.Iterator; import android.text.TextUtils; import com.facebook.stetho.dumpapp.ArgsHelper; import com.facebook.stetho.dumpapp.DumpException; import com.facebook.stetho.dumpapp.DumpUsageException; import com.facebook.stetho.dumpapp.DumperContext; import com.facebook.stetho.dumpapp.DumperPlugin; public class HelloWorldDumperPlugin implements DumperPlugin { private static final String NAME = "hello"; @Override public String getName() { return NAME; } @Override public void dump(DumperContext dumpContext) throws DumpException { PrintStream writer = dumpContext.getStdout(); Iterator args = dumpContext.getArgsAsList().iterator(); String helloToWhom = ArgsHelper.nextOptionalArg(args, null); if (helloToWhom != null) { doHello(dumpContext.getStdin(), writer, helloToWhom); } else { doUsage(writer); } } private void doHello(InputStream in, PrintStream writer, String name) throws DumpException { if (TextUtils.isEmpty(name)) { // This will print an error to the dumpapp user and cause a non-zero exit of the // script. throw new DumpUsageException("Name is empty"); } else if ("-".equals(name)) { try { name = new BufferedReader(new InputStreamReader(in)).readLine(); } catch (IOException e) { throw new DumpException(e.toString()); } } writer.println("Hello " + name + "!"); } private void doUsage(PrintStream writer) { writer.println("Usage: dumpapp " + NAME + " "); writer.println(); writer.println("If is '-', the name will be read from stdin."); } } ================================================ FILE: stetho-sample/src/debug/java/com/facebook/stetho/sample/SampleDebugApplication.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.sample; import android.annotation.TargetApi; import android.content.Context; import android.os.Build; import android.os.SystemClock; import android.provider.CalendarContract; import android.util.Log; import com.facebook.stetho.DumperPluginsProvider; import com.facebook.stetho.InspectorModulesProvider; import com.facebook.stetho.Stetho; import com.facebook.stetho.dumpapp.DumperPlugin; import com.facebook.stetho.inspector.database.ContentProviderDatabaseDriver; import com.facebook.stetho.inspector.database.ContentProviderSchema; import com.facebook.stetho.inspector.database.ContentProviderSchema.Table; import com.facebook.stetho.inspector.protocol.ChromeDevtoolsDomain; public class SampleDebugApplication extends SampleApplication { private static final String TAG = "SampleDebugApplication"; @Override public void onCreate() { super.onCreate(); long startTime = SystemClock.elapsedRealtime(); initializeStetho(this); long elapsed = SystemClock.elapsedRealtime() - startTime; Log.i(TAG, "Stetho initialized in " + elapsed + " ms"); } private void initializeStetho(final Context context) { // See also: Stetho.initializeWithDefaults(Context) Stetho.initialize(Stetho.newInitializerBuilder(context) .enableDumpapp(new DumperPluginsProvider() { @Override public Iterable get() { return new Stetho.DefaultDumperPluginsBuilder(context) .provide(new HelloWorldDumperPlugin()) .provide(new APODDumperPlugin(context.getContentResolver())) .finish(); } }) .enableWebKitInspector(new ExtInspectorModulesProvider(context)) .build()); } private static class ExtInspectorModulesProvider implements InspectorModulesProvider { private Context mContext; ExtInspectorModulesProvider(Context context) { mContext = context; } @Override public Iterable get() { return new Stetho.DefaultInspectorModulesBuilder(mContext) .provideDatabaseDriver(createContentProviderDatabaseDriver(mContext)) .finish(); } @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) private ContentProviderDatabaseDriver createContentProviderDatabaseDriver(Context context) { ContentProviderSchema calendarsSchema = new ContentProviderSchema.Builder() .table(new Table.Builder() .uri(CalendarContract.Calendars.CONTENT_URI) .projection(new String[] { CalendarContract.Calendars._ID, CalendarContract.Calendars.NAME, CalendarContract.Calendars.ACCOUNT_NAME, CalendarContract.Calendars.IS_PRIMARY, }) .build()) .build(); // sample events content provider we want to support ContentProviderSchema eventsSchema = new ContentProviderSchema.Builder() .table(new Table.Builder() .uri(CalendarContract.Events.CONTENT_URI) .projection(new String[]{ CalendarContract.Events._ID, CalendarContract.Events.TITLE, CalendarContract.Events.DESCRIPTION, CalendarContract.Events.ACCOUNT_NAME, CalendarContract.Events.DTSTART, CalendarContract.Events.DTEND, CalendarContract.Events.CALENDAR_ID, }) .build()) .build(); return new ContentProviderDatabaseDriver(context, calendarsSchema, eventsSchema); } } } ================================================ FILE: stetho-sample/src/main/AndroidManifest.xml ================================================ ================================================ FILE: stetho-sample/src/main/java/com/facebook/stetho/sample/APODActivity.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.sample; import java.io.IOException; import android.app.ListActivity; import android.app.LoaderManager; import android.content.Context; import android.content.CursorLoader; import android.content.Intent; import android.content.Loader; import android.database.CharArrayBuffer; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.drawable.BitmapDrawable; import android.os.Bundle; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.CursorAdapter; import android.widget.ImageView; import android.widget.TextView; /** * Simple demonstration of fetching and caching a specific RSS feed showing the * "Astronomy Picture of the Day" feed from NASA. This demonstrates both the database access * and network inspection features of Stetho. */ public class APODActivity extends ListActivity { private static final int LOADER_APOD_POSTS = 1; private static final String TAG = "APODActivity"; private APODPostsAdapter mAdapter; public static void show(Context context) { context.startActivity(new Intent(context, APODActivity.class)); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); getLoaderManager().initLoader(LOADER_APOD_POSTS, new Bundle(), mLoaderCallback); new APODRssFetcher(getContentResolver()).fetchAndStore(); mAdapter = new APODPostsAdapter(this); setListAdapter(mAdapter); } @Override protected void onDestroy() { super.onDestroy(); getLoaderManager().destroyLoader(LOADER_APOD_POSTS); } private final LoaderManager.LoaderCallbacks mLoaderCallback = new LoaderManager.LoaderCallbacks() { @Override public Loader onCreateLoader(int id, Bundle args) { switch (id) { case LOADER_APOD_POSTS: return APODPostsQuery.createCursorLoader(APODActivity.this); default: throw new IllegalArgumentException("id=" + id); } } @Override public void onLoadFinished(Loader loader, Cursor data) { mAdapter.changeCursor(data); } @Override public void onLoaderReset(Loader loader) { mAdapter.changeCursor(null); } }; private class APODPostsAdapter extends CursorAdapter { private final LayoutInflater mInflater; public APODPostsAdapter(Context context) { super(context, null /* cursor */, false /* autoRequery */); mInflater = LayoutInflater.from(context); } @Override public View newView(Context context, Cursor cursor, ViewGroup parent) { View view = mInflater.inflate(R.layout.apod_list_item, parent, false); view.setTag(new ViewHolder(view)); return view; } @Override public void bindView(View view, Context context, Cursor cursor) { ViewHolder holder = (ViewHolder)view.getTag(); int bindPosition = cursor.getPosition(); holder.position = bindPosition; final String imageUrl = cursor.getString(APODPostsQuery.LARGE_IMAGE_URL_INDEX); holder.image.setImageDrawable(null); fetchImage(imageUrl, bindPosition, holder); cursor.copyStringToBuffer(APODPostsQuery.TITLE_INDEX, holder.titleBuffer); setTextWithBuffer(holder.title, holder.titleBuffer); cursor.copyStringToBuffer(APODPostsQuery.DESCRIPTION_TEXT_INDEX, holder.descriptionBuffer); setTextWithBuffer(holder.description, holder.descriptionBuffer); } // Really crude image handling. Please don't do this in a real app :) private void fetchImage( final String imageUrl, final int bindPosition, final ViewHolder holder) { Networker.HttpRequest imageRequest = Networker.HttpRequest.newBuilder() .method(Networker.HttpMethod.GET) .url(imageUrl) .build(); Networker.get().submit(imageRequest, new Networker.Callback() { @Override public void onResponse(Networker.HttpResponse result) { if (bindPosition == holder.position) { Log.d(TAG, "Got " + imageUrl + ": " + result.statusCode + ", " + result.body.length); if (result.statusCode == 200) { final Bitmap bitmap = BitmapFactory.decodeByteArray(result.body, 0, result.body.length); APODActivity.this.runOnUiThread(new Runnable() { @Override public void run() { holder.image.setImageDrawable(new BitmapDrawable(bitmap)); } }); } } } @Override public void onFailure(IOException e) { // Let Stetho demonstrate the errors :) } }); } } private static class ViewHolder { public final ImageView image; public final TextView title; public final CharArrayBuffer titleBuffer = new CharArrayBuffer(32); public final TextView description; public final CharArrayBuffer descriptionBuffer = new CharArrayBuffer(64); int position; public ViewHolder(View v) { image = (ImageView)v.findViewById(R.id.image); title = (TextView)v.findViewById(R.id.title); description = (TextView)v.findViewById(R.id.description); } } private static void setTextWithBuffer(TextView textView, CharArrayBuffer buffer) { textView.setText(buffer.data, 0, buffer.sizeCopied); } private static class APODPostsQuery { public static String[] PROJECTION = { APODContract.Columns._ID, APODContract.Columns.TITLE, APODContract.Columns.DESCRIPTION_TEXT, APODContract.Columns.LARGE_IMAGE_URL, }; public static final int ID_INDEX = 0; public static final int TITLE_INDEX = 1; public static final int DESCRIPTION_TEXT_INDEX = 2; public static final int LARGE_IMAGE_URL_INDEX = 3; public static CursorLoader createCursorLoader(Context context) { return new CursorLoader( context, APODContract.CONTENT_URI, PROJECTION, null /* selection */, null /* selectionArgs */, null /* sortOrder */); } } } ================================================ FILE: stetho-sample/src/main/java/com/facebook/stetho/sample/APODContentProvider.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.sample; import android.content.ContentProvider; import android.content.ContentProviderOperation; import android.content.ContentProviderResult; import android.content.ContentValues; import android.content.Context; import android.content.OperationApplicationException; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.net.Uri; import java.util.ArrayList; public class APODContentProvider extends ContentProvider { private APODSQLiteOpenHelper mOpenHelper; @Override public boolean onCreate() { mOpenHelper = new APODSQLiteOpenHelper(getContext()); return true; } @Override public Cursor query( Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { SQLiteDatabase db = mOpenHelper.getWritableDatabase(); Cursor cursor = db.query( APODContract.TABLE_NAME, projection, selection, selectionArgs, null /* groupBy */, null /* having */, sortOrder, null /* limit */); cursor.setNotificationUri(getContext().getContentResolver(), APODContract.CONTENT_URI); return cursor; } @Override public String getType(Uri uri) { return null; } @Override public Uri insert(Uri uri, ContentValues values) { SQLiteDatabase db = mOpenHelper.getWritableDatabase(); long id = db.insert(APODContract.TABLE_NAME, null /* nullColumnHack */, values); notifyChange(); return uri.buildUpon().appendEncodedPath(String.valueOf(id)).build(); } @Override public int delete(Uri uri, String selection, String[] selectionArgs) { SQLiteDatabase db = mOpenHelper.getWritableDatabase(); int count = db.delete(APODContract.TABLE_NAME, selection, selectionArgs); notifyChange(); return count; } @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { SQLiteDatabase db = mOpenHelper.getWritableDatabase(); int count = db.update(APODContract.TABLE_NAME, values, selection, selectionArgs); notifyChange(); return count; } @Override public ContentProviderResult[] applyBatch(ArrayList operations) throws OperationApplicationException { SQLiteDatabase db = mOpenHelper.getWritableDatabase(); db.beginTransaction(); try { ContentProviderResult[] results = super.applyBatch(operations); db.setTransactionSuccessful(); return results; } finally { db.endTransaction(); notifyChange(); } } private void notifyChange() { getContext().getContentResolver().notifyChange(APODContract.CONTENT_URI, null /* observer */); } private static class APODSQLiteOpenHelper extends SQLiteOpenHelper { private static final String DB_NAME = "apod.db"; private static final int DB_VERSION = 2; public APODSQLiteOpenHelper(Context context) { super(context, DB_NAME, null /* factory */, DB_VERSION); } @Override public void onCreate(SQLiteDatabase db) { db.execSQL( "CREATE TABLE " + APODContract.TABLE_NAME + " (" + APODContract.Columns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + APODContract.Columns.TITLE + " TEXT, " + APODContract.Columns.DESCRIPTION_IMAGE_URL + " TEXT, " + APODContract.Columns.DESCRIPTION_TEXT + " TEXT, " + APODContract.Columns.LARGE_IMAGE_URL + " TEXT " + ")"); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { drop(db); onCreate(db); } private void drop(SQLiteDatabase db) { db.execSQL("DROP TABLE " + APODContract.TABLE_NAME); } } } ================================================ FILE: stetho-sample/src/main/java/com/facebook/stetho/sample/APODContract.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.sample; import android.net.Uri; import android.provider.BaseColumns; public interface APODContract { String AUTHORITY = "com.facebook.stetho.sample.apod"; Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY); String TABLE_NAME = "rss_items"; interface Columns extends BaseColumns { String TITLE = "title"; String DESCRIPTION_TEXT = "description_text"; String DESCRIPTION_IMAGE_URL = "description_image_url"; String LARGE_IMAGE_URL = "large_image_url"; } } ================================================ FILE: stetho-sample/src/main/java/com/facebook/stetho/sample/APODRssFetcher.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.sample; import android.content.ContentProviderOperation; import android.content.ContentResolver; import android.content.ContentValues; import android.content.OperationApplicationException; import android.net.Uri; import android.os.RemoteException; import android.util.Log; import android.util.Xml; import com.facebook.stetho.common.Utf8Charset; import com.facebook.stetho.common.Util; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import javax.annotation.Nullable; import java.io.ByteArrayInputStream; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; public class APODRssFetcher { private static final String TAG = "APODRssFetcher"; private static final String APOD_RSS_URL = "https://apod.nasa.gov/apod.rss"; private final ContentResolver mContentResolver; public APODRssFetcher(ContentResolver contentResolver) { mContentResolver = contentResolver; } public void fetchAndStore() { Networker.HttpRequest request = Networker.HttpRequest.newBuilder() .friendlyName("APOD RSS") .method(Networker.HttpMethod.GET) .url(APOD_RSS_URL) .build(); Networker.get().submit(request, mStoreRssResponse); } private final Networker.Callback mStoreRssResponse = new Networker.Callback() { @Override public void onResponse(Networker.HttpResponse result) { if (result.statusCode == 200) { try { List rssItems = parseRss(result.body); List apodItems = decorateRssItemsWithLinkImages(rssItems); store(apodItems); } catch (XmlPullParserException e) { Log.e(TAG, "Parse error", e); } catch (OperationApplicationException e) { Log.e(TAG, "Database write error", e); } catch (RemoteException e) { // Not recoverable, our process or the system_server must be dying... throw new RuntimeException(e); } catch (IOException e) { // Reading from a byte[] shouldn't cause this... throw new RuntimeException(e); } } } @Override public void onFailure(IOException e) { // Show in Stetho :) } private List parseRss(byte[] body) throws IOException, XmlPullParserException { XmlPullParser parser = Xml.newPullParser(); parser.setInput(new ByteArrayInputStream(body), "UTF-8"); List items = new RssParser(parser).parse(); Log.d(TAG, "Fetched " + items.size() + " items"); return items; } public List decorateRssItemsWithLinkImages(List rssItems) { ArrayList apodItems = new ArrayList<>(rssItems.size()); final CountDownLatch fetchLinkLatch = new CountDownLatch(rssItems.size()); for (RssItem rssItem : rssItems) { final ApodItem apodItem = new ApodItem(); apodItem.rssItem = rssItem; fetchLinkPage(rssItem.link, new PageScrapedCallback() { @Override public void onPageScraped(@Nullable List imageUrls) { apodItem.largeImageUrl = imageUrls != null && !imageUrls.isEmpty() ? imageUrls.get(0) : null; fetchLinkLatch.countDown(); } }); apodItems.add(apodItem); } // Wait for all link fetches to complete, despite running them in parallel... Util.awaitUninterruptibly(fetchLinkLatch); return apodItems; } private void store(List items) throws RemoteException, OperationApplicationException { ArrayList operations = new ArrayList(); operations.add( ContentProviderOperation.newDelete(APODContract.CONTENT_URI) .build()); for (ApodItem item : items) { Log.d(TAG, "Add item: " + item.rssItem.title); operations.add( ContentProviderOperation.newInsert(APODContract.CONTENT_URI) .withValues(convertItemToValues(item)) .build()); } mContentResolver.applyBatch(APODContract.AUTHORITY, operations); } private ContentValues convertItemToValues(ApodItem item) { ContentValues values = new ContentValues(); values.put(APODContract.Columns.TITLE, item.rssItem.title); ArrayList imageUrls = new ArrayList<>(); String strippedText = HtmlScraper.parseWithImageTags( item.rssItem.description, null /* origin */, imageUrls); // Hack to remove some strange non-printing character at the start... strippedText = strippedText.substring(1).trim(); String imageUrl = !imageUrls.isEmpty() ? imageUrls.get(0) : null; values.put(APODContract.Columns.DESCRIPTION_IMAGE_URL, imageUrl); values.put(APODContract.Columns.DESCRIPTION_TEXT, strippedText); values.put(APODContract.Columns.LARGE_IMAGE_URL, item.largeImageUrl); return values; } }; private void fetchLinkPage(String linkUrl, PageScrapedCallback callback) { String originUrl = getOriginUri(Uri.parse(linkUrl)).toString(); Networker.HttpRequest request = Networker.HttpRequest.newBuilder() .friendlyName("fetchLinkPage") .method(Networker.HttpMethod.GET) .url(linkUrl) .build(); Networker.get().submit(request, new PageScrapeNetworkCallback(originUrl, callback)); } private static Uri getOriginUri(Uri uri) { Uri.Builder b = uri.buildUpon(); b.encodedPath(null); List segments = uri.getPathSegments(); for (int i = 0; i < segments.size() - 1; i++) { b.appendEncodedPath(segments.get(i)); } return b.build(); } private static class PageScrapeNetworkCallback implements Networker.Callback { @Nullable private final String mOrigin; private final PageScrapedCallback mDelegate; public PageScrapeNetworkCallback(@Nullable String origin, PageScrapedCallback delegate) { mOrigin = origin; mDelegate = delegate; } @Override public void onResponse(Networker.HttpResponse result) { ArrayList imageUrls = new ArrayList<>(); String htmlText = Utf8Charset.decodeUTF8(result.body); HtmlScraper.parseWithImageTags(htmlText, mOrigin, imageUrls); mDelegate.onPageScraped(imageUrls); } @Override public void onFailure(IOException e) { mDelegate.onPageScraped(null /* imageUrls */); } } private static class RssParser { private final XmlPullParser mParser; public RssParser(XmlPullParser parser) { mParser = parser; } public List parse() throws IOException, XmlPullParserException { ArrayList items = new ArrayList(); mParser.nextTag(); mParser.require(XmlPullParser.START_TAG, null, "rss"); mParser.nextTag(); mParser.require(XmlPullParser.START_TAG, null, "channel"); while (mParser.next() != XmlPullParser.END_TAG) { if (mParser.getEventType() != XmlPullParser.START_TAG) { continue; } String name = mParser.getName(); if (name.equals("item")) { items.add(readItem()); } else { skip(); } } return items; } private RssItem readItem() throws XmlPullParserException, IOException { mParser.require(XmlPullParser.START_TAG, null, "item"); RssItem item = new RssItem(); while (mParser.next() != XmlPullParser.END_TAG) { if (mParser.getEventType() != XmlPullParser.START_TAG) { continue; } String name = mParser.getName(); if (name.equals("title")) { item.title = readTextFromTag("title"); } else if (name.equals("description")) { item.description = readTextFromTag("description"); } else if (name.equals("link")) { item.link = readTextFromTag("link"); } else { skip(); } } return item; } private String readTextFromTag(String tagName) throws IOException, XmlPullParserException { mParser.require(XmlPullParser.START_TAG, null, tagName); String text = readText(); mParser.require(XmlPullParser.END_TAG, null, tagName); return text; } private String readText() throws IOException, XmlPullParserException { String result = ""; if (mParser.next() == XmlPullParser.TEXT) { result = mParser.getText(); mParser.nextTag(); } return result; } private void skip() throws IOException, XmlPullParserException { if (mParser.getEventType() != XmlPullParser.START_TAG) { throw new IllegalStateException(); } int depth = 1; while (depth != 0) { switch (mParser.next()) { case XmlPullParser.END_TAG: depth--; break; case XmlPullParser.START_TAG: depth++; break; } } } } private static class RssItem { public String title; public String description; public String link; } private static class ApodItem { public RssItem rssItem; @Nullable public String largeImageUrl; } private interface PageScrapedCallback { /** * @param imageUrls Image URLs that were scraped or null if the page could not be fetched or * parsed. */ public void onPageScraped(@Nullable List imageUrls); } } ================================================ FILE: stetho-sample/src/main/java/com/facebook/stetho/sample/Constants.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.sample; public class Constants { public static final String TAG = "StethoSample"; } ================================================ FILE: stetho-sample/src/main/java/com/facebook/stetho/sample/HtmlScraper.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.sample; import android.graphics.Color; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.net.Uri; import android.text.Html; import android.text.TextUtils; import javax.annotation.Nullable; import java.util.List; public class HtmlScraper { /** * Scrapes an HTML page for <img> tags. * * @return Scraped plain text */ public static String parseWithImageTags( String htmlText, @Nullable String originUrl, List outImageUrls) { ExtractImageGetter imageGetter = new ExtractImageGetter(originUrl, outImageUrls); String strippedText = Html.fromHtml( htmlText, imageGetter, null /* tagHandler */) .toString(); return strippedText.trim(); } private static class ExtractImageGetter implements Html.ImageGetter { @Nullable private final String mOriginUrl; private final List mSources; public ExtractImageGetter(@Nullable String originUrl, List outSources) { mOriginUrl = originUrl; mSources = outSources; } @Override public Drawable getDrawable(String source) { if (mOriginUrl != null && TextUtils.isEmpty(Uri.parse(source).getScheme())) { StringBuilder newSource = new StringBuilder(); newSource.append(mOriginUrl); if (!mOriginUrl.endsWith("/") && !source.startsWith("/")) { newSource.append("/"); } newSource.append(source); source = newSource.toString(); } mSources.add(source); // Dummy drawable. return new ColorDrawable(Color.TRANSPARENT); } public List getSources() { return mSources; } } } ================================================ FILE: stetho-sample/src/main/java/com/facebook/stetho/sample/IRCChatActivity.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.sample; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.text.TextUtils; import android.view.KeyEvent; import android.view.View; import android.view.inputmethod.EditorInfo; import android.widget.ArrayAdapter; import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; import java.io.IOException; import java.util.Locale; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import javax.annotation.Nullable; public class IRCChatActivity extends Activity { private static final int DEFAULT_PORT = 6667; private static final String EXTRA_HOST_AND_MAYBE_PORT = "host"; private static final String EXTRA_NICKNAME = "nickname"; private SimpleIRCConnectionManager mSimpleIRCConnectionManager; private ExecutorService mConnectionExecutor; private boolean mIsTearingDown; private ListView mConsoleDisplay; private IRCConsoleRowAdapter mConsoleRowAdapter; private TextView mConsoleInput; public static void showForResult( Activity context, int requestCode, String hostAndMaybePort, String nickname) { Intent intent = new Intent(context, IRCChatActivity.class); intent.putExtra(EXTRA_HOST_AND_MAYBE_PORT, hostAndMaybePort); intent.putExtra(EXTRA_NICKNAME, nickname); context.startActivityForResult(intent, requestCode); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.irc_chat_activity); mConsoleDisplay = (ListView) findViewById(R.id.console_display); mConsoleRowAdapter = new IRCConsoleRowAdapter(this); mConsoleDisplay.setAdapter(mConsoleRowAdapter); mConsoleInput = (TextView) findViewById(R.id.console_input); mConsoleInput.setOnEditorActionListener(mOnConsoleInputEditorAction); findViewById(R.id.console_send).setOnClickListener(mConsoleSendClicked); // Will re-enable once we connect... mConsoleInput.setEnabled(false); mSimpleIRCConnectionManager = new SimpleIRCConnectionManager( getIntent().getStringExtra(EXTRA_HOST_AND_MAYBE_PORT), getIntent().getStringExtra(EXTRA_NICKNAME)); mConnectionExecutor = Executors.newCachedThreadPool(); mConnectionExecutor.execute(new Runnable() { @Override public void run() { mSimpleIRCConnectionManager.runConnectLoop(); } }); } @Override protected void onDestroy() { mSimpleIRCConnectionManager.shutdown(); mConnectionExecutor.shutdown(); mIsTearingDown = true; super.onDestroy(); } private void onConnected() { mConsoleInput.setEnabled(true); } private void onIncomingMessage(String message) { if (mIsTearingDown) { return; } mConsoleRowAdapter.add(message); } private void onDisconnectOrConnectFailed(@Nullable IOException exception) { if (mIsTearingDown) { return; } mIsTearingDown = true; final String error; if (exception != null) { Toast.makeText( this, "Error: " + exception.getMessage(), Toast.LENGTH_LONG) .show(); error = exception.getMessage(); } else { error = null; } new IRCChatActivityResult(error).setResult(this); finish(); } private final TextView.OnEditorActionListener mOnConsoleInputEditorAction = new TextView.OnEditorActionListener() { @Override public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { switch (actionId) { case EditorInfo.IME_ACTION_SEND: doSendMessage(); return true; default: return false; } } }; private final View.OnClickListener mConsoleSendClicked = new View.OnClickListener() { @Override public void onClick(View v) { doSendMessage(); } }; private void doSendMessage() { final String message = mConsoleInput.getText().toString(); mConsoleInput.setText(""); if (!TextUtils.isEmpty(message)) { mConnectionExecutor.execute(new Runnable() { @Override public void run() { mSimpleIRCConnectionManager.send(message); } }); } } private class SimpleIRCConnectionManager { @Nullable private volatile IRCClientConnection mConnection; private volatile boolean mShutdownRequested; private final String mHost; private final int mPort; private final String mNickname; public SimpleIRCConnectionManager(String hostAndPort, String nickname) { String[] hostAndPortParts = hostAndPort.split(":", 2); if (hostAndPortParts.length == 2) { mHost = hostAndPortParts[0]; mPort = Integer.parseInt(hostAndPortParts[1]); } else { mHost = hostAndPort; mPort = DEFAULT_PORT; } mNickname = nickname; } public void runConnectLoop() { boolean graceful = false; try { final IRCClientConnection conn = IRCClientConnection.connect(mHost, mPort); try { mConnection = conn; doConnectLoop(conn); } finally { mConnection = null; conn.close(); } graceful = true; } catch (IOException e) { invokeOnDisconnectOrConnectFailed(e); } finally { if (graceful) { invokeOnDisconnectOrConnectFailed(null /* exception */); } } } private void doConnectLoop(IRCClientConnection conn) throws IOException { if (mShutdownRequested) { return; } runOnUiThread(new Runnable() { @Override public void run() { onConnected(); } }); conn.send(String.format(Locale.US, "NICK %s", mNickname)); conn.send(String.format(Locale.US, "USER %s %s blablabla :%s", mNickname, mHost, mNickname)); while (!mShutdownRequested) { final String message = conn.read(); if (message == null) { break; } runOnUiThread(new Runnable() { @Override public void run() { onIncomingMessage(message); } }); } } public void send(String message) { IRCClientConnection conn = mConnection; if (conn != null) { try { conn.send(message); } catch (IOException e) { invokeOnDisconnectOrConnectFailed(e); } } } public void shutdown() { mShutdownRequested = true; // Force a socket closure to cause an immediate effect in Stetho. IRCClientConnection conn = mConnection; if (conn != null) { try { conn.close(); } catch (IOException e) { invokeOnDisconnectOrConnectFailed(e); } } } private void invokeOnDisconnectOrConnectFailed(@Nullable final IOException e) { runOnUiThread(new Runnable() { @Override public void run() { onDisconnectOrConnectFailed(e); } }); } } private static class IRCConsoleRowAdapter extends ArrayAdapter { public IRCConsoleRowAdapter(Context context) { super(context, R.layout.irc_console_row); } } public static class IRCChatActivityResult { private static final String EXTRA_RESULT_CONNECT_ERROR = "error"; @Nullable public final String connectError; public static IRCChatActivityResult fromResult(int resultCode, Intent data) { if (resultCode == RESULT_CANCELED) { return new IRCChatActivityResult(null /* connectError */); } else { return new IRCChatActivityResult(data.getStringExtra(EXTRA_RESULT_CONNECT_ERROR)); } } public IRCChatActivityResult(@Nullable String connectError) { this.connectError = connectError; } public boolean wasUserDisconnect() { return connectError == null; } private void setResult(Activity activity) { if (wasUserDisconnect()) { activity.setResult(RESULT_CANCELED); } else { activity.setResult( RESULT_OK, new Intent(activity, IRCChatActivity.class) .putExtra(EXTRA_RESULT_CONNECT_ERROR, connectError)); } } } } ================================================ FILE: stetho-sample/src/main/java/com/facebook/stetho/sample/IRCClientConnection.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.sample; import com.facebook.stetho.inspector.network.NetworkEventReporter; import com.facebook.stetho.inspector.network.NetworkEventReporterImpl; import com.facebook.stetho.inspector.network.SimpleTextInspectorWebSocketFrame; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.Closeable; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.net.InetSocketAddress; import java.net.Socket; import javax.annotation.Nullable; /** * Simple IRC client connection system to demonstrate Stetho's "websocket" (any arbitrary * socket will work too) support. */ public class IRCClientConnection implements Closeable { private final StethoReporter mReporter; private final Socket mSocket; private final BufferedReader mInput; private final BufferedWriter mOutput; public static IRCClientConnection connect(String host, int port) throws IOException { StethoReporter reporter = new StethoReporter(); Socket socket = new Socket(); reporter.onPreConnect(host, port); try { socket.connect(new InetSocketAddress(host, port)); reporter.onPostConnect(); } catch (IOException e) { reporter.onError(e); try { socket.close(); throw e; } finally { reporter.onClosed(); } } return new IRCClientConnection(reporter, socket, "UTF-8"); } private IRCClientConnection( StethoReporter reporter, Socket socket, String charset) throws IOException { mReporter = reporter; mSocket = socket; mInput = new BufferedReader(new InputStreamReader(socket.getInputStream(), charset)); mOutput = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), charset)); } @Nullable public String read() throws IOException { try { String message = mInput.readLine(); if (message != null) { mReporter.onReceive(message); maybeHandleIncomingMessage(message); } return message; } catch (IOException e) { mReporter.onError(e); throw e; } } public void send(String message) throws IOException { mReporter.onSend(message); try { mOutput.write(message + "\r\n"); mOutput.flush(); } catch (IOException e) { mReporter.onError(e); throw e; } } private boolean maybeHandleIncomingMessage(String message) throws IOException { if (message.startsWith("PING ")) { send("PONG " + message.substring("PING ".length())); return true; } return false; } public void close() throws IOException { try { try { mOutput.close(); } catch (IOException e) { mReporter.onError(e); throw e; } } finally { try { mSocket.close(); } catch (IOException e) { mReporter.onError(e); // Technically this should use addSuppressed but it's KITKAT only and this is a demo... throw e; } finally { mReporter.onClosed(); } } } private static class StethoReporter { private final NetworkEventReporter mReporter; private final String mRequestId; public StethoReporter() { mReporter = NetworkEventReporterImpl.get(); mRequestId = mReporter.nextRequestId(); } public void onPreConnect(String host, int port) { mReporter.webSocketCreated(mRequestId, "irc://" + host + ":" + port); } public void onPostConnect() { // Sadly, nothing to report... } public void onError(IOException e) { mReporter.webSocketFrameError(mRequestId, e.getMessage()); } public void onClosed() { mReporter.webSocketClosed(mRequestId); } public void onSend(String message) { mReporter.webSocketFrameSent(new SimpleTextInspectorWebSocketFrame(mRequestId, message)); } public void onReceive(String message) { mReporter.webSocketFrameReceived(new SimpleTextInspectorWebSocketFrame(mRequestId, message)); } } } ================================================ FILE: stetho-sample/src/main/java/com/facebook/stetho/sample/IRCConnectActivity.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.sample; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.text.TextUtils; import android.view.View; import android.widget.EditText; import android.widget.TextView; import com.facebook.stetho.sample.IRCChatActivity.IRCChatActivityResult; import java.util.Random; public class IRCConnectActivity extends Activity { private static final String DEFAULT_HOST = "irc.freenode.net"; private static final int REQUEST_CODE_CHAT = 1; private TextView mIRCPriorError; private EditText mIRCServer; private EditText mIRCNickname; public static void show(Context context) { Intent intent = new Intent(context, IRCConnectActivity.class); context.startActivity(intent); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.irc_connect_activity); mIRCPriorError = (TextView) findViewById(R.id.irc_prior_error); mIRCServer = (EditText) findViewById(R.id.irc_server); if (TextUtils.isEmpty(mIRCServer.getText())) { mIRCServer.setText(DEFAULT_HOST); } mIRCNickname = (EditText) findViewById(R.id.irc_nickname); if (TextUtils.isEmpty(mIRCNickname.getText())) { mIRCNickname.setText("stetho" + (new Random().nextInt(9999) + 1)); } findViewById(R.id.irc_connect).setOnClickListener(mConnectClicked); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { switch (requestCode) { case REQUEST_CODE_CHAT: IRCChatActivityResult parsedResult = IRCChatActivityResult.fromResult(resultCode, data); if (parsedResult.wasUserDisconnect()) { mIRCPriorError.setText(""); mIRCPriorError.setVisibility(View.GONE); } else { mIRCPriorError.setText("ERROR: " + parsedResult.connectError); mIRCPriorError.setVisibility(View.VISIBLE); } break; default: throw new IllegalArgumentException("Unknown requestCode=" + requestCode); } } private final View.OnClickListener mConnectClicked = new View.OnClickListener() { @Override public void onClick(View v) { IRCChatActivity.showForResult( IRCConnectActivity.this, REQUEST_CODE_CHAT, mIRCServer.getText().toString(), mIRCNickname.getText().toString()); } }; } ================================================ FILE: stetho-sample/src/main/java/com/facebook/stetho/sample/MainActivity.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.sample; import android.app.Activity; import android.app.Dialog; import android.content.SharedPreferences; import android.os.Bundle; import android.preference.PreferenceManager; import android.view.LayoutInflater; import android.view.View; import android.widget.Toast; public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main_activity); // Demonstrate that it is removed from the release build... if (!isStethoPresent()) { Toast.makeText( this, getString(R.string.stetho_missing, BuildConfig.BUILD_TYPE), Toast.LENGTH_LONG) .show(); } findViewById(R.id.settings_btn).setOnClickListener(mMainButtonClicked); findViewById(R.id.apod_btn).setOnClickListener(mMainButtonClicked); findViewById(R.id.irc_btn).setOnClickListener(mMainButtonClicked); findViewById(R.id.about).setOnClickListener(mMainButtonClicked); } private static boolean isStethoPresent() { try { Class.forName("com.facebook.stetho.Stetho"); return true; } catch (ClassNotFoundException e) { return false; } } @Override protected void onResume() { super.onResume(); getPrefs().registerOnSharedPreferenceChangeListener(mToastingPrefListener); } @Override protected void onPause() { super.onPause(); getPrefs().unregisterOnSharedPreferenceChangeListener(mToastingPrefListener); } private SharedPreferences getPrefs() { return PreferenceManager.getDefaultSharedPreferences(this /* context */); } private final View.OnClickListener mMainButtonClicked = new View.OnClickListener() { @Override public void onClick(View v) { int id = v.getId(); if (id == R.id.settings_btn) { SettingsActivity.show(MainActivity.this); } else if (id == R.id.apod_btn) { APODActivity.show(MainActivity.this); } else if (id == R.id.irc_btn) { IRCConnectActivity.show(MainActivity.this); } else if (id == R.id.about) { View view = LayoutInflater.from(MainActivity.this).inflate(R.layout.dialog_layout, null); Dialog dialog = new Dialog(MainActivity.this); dialog.setContentView(view); dialog.setTitle(getString(R.string.app_name)); dialog.show(); } } }; private final SharedPreferences.OnSharedPreferenceChangeListener mToastingPrefListener = new SharedPreferences.OnSharedPreferenceChangeListener() { @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { Object value = sharedPreferences.getAll().get(key); Toast.makeText( MainActivity.this, getString(R.string.pref_change_message, key, value), Toast.LENGTH_SHORT).show(); } }; } ================================================ FILE: stetho-sample/src/main/java/com/facebook/stetho/sample/Networker.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ 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); } } ================================================ FILE: stetho-sample/src/main/java/com/facebook/stetho/sample/SampleApplication.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.sample; import android.app.Application; public class SampleApplication extends Application { @Override public void onCreate() { super.onCreate(); // Your normal application code here. See SampleDebugApplication for Stetho initialization. } } ================================================ FILE: stetho-sample/src/main/java/com/facebook/stetho/sample/SettingsActivity.java ================================================ /* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.stetho.sample; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.preference.PreferenceActivity; public class SettingsActivity extends PreferenceActivity { public static void show(Context context) { context.startActivity(new Intent(context, SettingsActivity.class)); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Trying to avoid a dependency on the support library and go all the way back to Gingerbread, // so we can't rely on the fragment-based preferences and must use the old deprecated methods. addPreferencesFromResource(R.xml.settings); } } ================================================ FILE: stetho-sample/src/main/res/layout/apod_list_item.xml ================================================ ================================================ FILE: stetho-sample/src/main/res/layout/dialog_layout.xml ================================================ ================================================ FILE: stetho-sample/src/main/res/layout/irc_chat_activity.xml ================================================