Initial commit 🎉
fbshipit-source-id: b6fc29740c6875d2e78953b8a7123890a67930f2 Co-authored-by: Sebastian McKenzie <sebmck@fb.com> Co-authored-by: John Knox <jknox@fb.com> Co-authored-by: Emil Sjölander <emilsj@fb.com> Co-authored-by: Pritesh Nandgaonkar <prit91@fb.com>
This commit is contained in:
8
.eslintignore
Normal file
8
.eslintignore
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
src/fb/plugins/relaydevtools/relay-devtools/*
|
||||||
|
latest
|
||||||
|
resources
|
||||||
|
templates
|
||||||
|
node_modules
|
||||||
|
flow-typed
|
||||||
|
lib
|
||||||
|
!.eslintrc.js
|
||||||
30
.eslintrc.js
Normal file
30
.eslintrc.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2018-present Facebook.
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
* @format
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fbjs = require('eslint-config-fbjs');
|
||||||
|
|
||||||
|
// enforces copyright header and @format directive to be present in every file
|
||||||
|
const pattern = /^\*\n \* Copyright 2018-present Facebook\.\n \* This source code is licensed under the MIT license found in the\n \* LICENSE file in the root directory of this source tree\.\n \* @format\n./;
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
extends: 'fbjs',
|
||||||
|
plugins: [...fbjs.plugins, 'header', 'prettier'],
|
||||||
|
rules: {
|
||||||
|
// disable rules from eslint-config-fbjs
|
||||||
|
'react/react-in-jsx-scope': 0, // not needed with our metro implementation
|
||||||
|
'no-new': 0, // new keyword needed e.g. new Notification
|
||||||
|
'no-catch-shadow': 0, // only relevant for IE8 and below
|
||||||
|
'no-bitwise': 0, // bitwise operations needed in some places
|
||||||
|
'consistent-return': 0,
|
||||||
|
'max-len': 0, // let's take prettier take care of this
|
||||||
|
indent: 0, // let's take prettier take care of this
|
||||||
|
|
||||||
|
// additional rules for this project
|
||||||
|
'header/header': [2, 'block', {pattern}],
|
||||||
|
'prettier/prettier': [2, 'fb', '@format'],
|
||||||
|
},
|
||||||
|
};
|
||||||
24
.flowconfig
Normal file
24
.flowconfig
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
[ignore]
|
||||||
|
.*/scripts/.*
|
||||||
|
.*/coverage/.*
|
||||||
|
.*/node_modules/.*
|
||||||
|
.*/build/.*
|
||||||
|
.*/dist/.*
|
||||||
|
.*/static/.*
|
||||||
|
<PROJECT_ROOT>/src/fb/plugins/relaydevtools/relay-devtools/DevtoolsUI.js$
|
||||||
|
.*/website/.*
|
||||||
|
|
||||||
|
[libs]
|
||||||
|
lib
|
||||||
|
flow-typed
|
||||||
|
|
||||||
|
[options]
|
||||||
|
esproposal.export_star_as=enable
|
||||||
|
module.use_strict=true
|
||||||
|
emoji=true
|
||||||
|
all=true
|
||||||
|
include_warnings=true
|
||||||
|
module.name_mapper='sonar' -> '<PROJECT_ROOT>/src/index.js'
|
||||||
|
|
||||||
|
[version]
|
||||||
|
0.69.0
|
||||||
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
website/build
|
||||||
|
*.xcworkspace
|
||||||
|
**/Pods/
|
||||||
|
**/xcuserdata/
|
||||||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "libs/folly"]
|
||||||
|
path = libs/folly
|
||||||
|
url = https://github.com/facebook/folly.git
|
||||||
1
.sonarhandles
Normal file
1
.sonarhandles
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"osx": "GICWmAAS05ZnyDsCAAAAAABHIak5bnw7AAAg", "linux": "GICWmACHSiVuyDsCAAAAAABVAX1Bbnw7AAAg", "windows": "GICWmAAn5YB0yDsCAAAAAAAMdghhbnw7AAAg"}
|
||||||
56
.travis.yml
Normal file
56
.travis.yml
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
os: osx
|
||||||
|
osx_image: xcode9.3
|
||||||
|
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- language: node_js
|
||||||
|
node_js:
|
||||||
|
- "8"
|
||||||
|
|
||||||
|
install:
|
||||||
|
- yarn
|
||||||
|
- cd website
|
||||||
|
- yarn
|
||||||
|
- cd ..
|
||||||
|
|
||||||
|
script:
|
||||||
|
- yarn lint
|
||||||
|
- yarn build macOnly build-number=$TRAVIS_BUILD_NUMBER
|
||||||
|
- cd website
|
||||||
|
- yarn build
|
||||||
|
- cd ..
|
||||||
|
|
||||||
|
before_deploy:
|
||||||
|
- export SONAR_VERSION="v$(plutil -p $TRAVIS_BUILD_DIR/dist/mac/Sonar.app/Contents/Info.plist | awk '/CFBundleShortVersionString/ {print substr($3, 2, length($3)-2)}')"
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
- provider: pages
|
||||||
|
skip-cleanup: true
|
||||||
|
github-token: $GITHUB_TOKEN
|
||||||
|
fqdn: fbsonar.com
|
||||||
|
local-dir: website/build/sonar
|
||||||
|
keep-history: true
|
||||||
|
on:
|
||||||
|
branch: master
|
||||||
|
- provider: releases
|
||||||
|
api_key: $GITHUB_TOKEN
|
||||||
|
file: dist/Sonar.zip
|
||||||
|
name: $SONAR_VERSION
|
||||||
|
draft: true
|
||||||
|
skip_cleanup: true
|
||||||
|
on:
|
||||||
|
branch: master
|
||||||
|
|
||||||
|
- language: objective-c
|
||||||
|
|
||||||
|
before_install:
|
||||||
|
- pod repo update
|
||||||
|
|
||||||
|
install:
|
||||||
|
- cd iOS/Sample
|
||||||
|
- pod install
|
||||||
|
- cd ../../
|
||||||
|
|
||||||
|
script:
|
||||||
|
- cd iOS/Sample
|
||||||
|
- xcodebuild clean build -workspace Sample.xcworkspace -scheme Pods-Sample
|
||||||
12
.vscode/launch.json
vendored
Normal file
12
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Attach to Running Renderer",
|
||||||
|
"type": "chrome",
|
||||||
|
"request": "attach",
|
||||||
|
"port": 9222,
|
||||||
|
"webRoot": "${workspaceRoot}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
3
CODE_OF_CONDUCT.md
Normal file
3
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# 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.facebook.com/codeofconduct) so that you can understand what actions will and will not be tolerated.
|
||||||
55
CONTRIBUTING.md
Normal file
55
CONTRIBUTING.md
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# Contributing to Sonar
|
||||||
|
|
||||||
|
We want to make contributing to this project as easy and transparent as
|
||||||
|
possible.
|
||||||
|
|
||||||
|
## Our Development Process
|
||||||
|
|
||||||
|
Changes from Facebook employees are synced to the GitHub repo automatically.
|
||||||
|
PRs from the community are imported into our internal source control and then
|
||||||
|
pushed to GitHub.
|
||||||
|
|
||||||
|
For changes affecting both, native code and JavaScript, make sure to only create
|
||||||
|
a single PR containing both parts of the code.
|
||||||
|
|
||||||
|
Although the Sonar desktop app is only released for macOS right now, it is
|
||||||
|
possible to create Windows and Linux builds of the app. Please keep this in mind
|
||||||
|
when dealing with platform-specific code.
|
||||||
|
|
||||||
|
## 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: <https://code.facebook.com/cla>
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
We are using Prettier to format our source code. The styles are enforced via
|
||||||
|
eslint. Make sure everything is well formatted before creating a PR. Therefore,
|
||||||
|
run `yarn lint` and `yarn fix` to apply formatting fixes.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
By contributing to Sonar, you agree that your contributions will be licensed
|
||||||
|
under the LICENSE file in the root directory of this source tree.
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2004-present, Facebook, Inc.
|
||||||
|
|
||||||
|
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.
|
||||||
54
README.md
Normal file
54
README.md
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
Sonar is a desktop app and client API for real time debugging of mobile apps. Sonar enables developers to quickly build plugins which expose runtime information from Android and iOS apps in a easy to use desktop interface.
|
||||||
|
|
||||||
|
# Why should I build tools on top of Sonar?
|
||||||
|
|
||||||
|
Sonar provides you with everything you need to quickly get your tools into people hands. Sonar provides a simple API for communicating with both Android and iOS devices, reconnecting if connection is lost as well as managing multiple connections. With Sonar's built in set of components it is easy to make good looking, easy to use, and powerful developer tools.
|
||||||
|
|
||||||
|
# In this repo
|
||||||
|
|
||||||
|
This repository includes all parts of Sonar. This includes:
|
||||||
|
|
||||||
|
* Sonar's desktop app built using Electron (`/src`)
|
||||||
|
* native Sonar SDKs for iOS
|
||||||
|
* native Sonar SDKs for Android
|
||||||
|
* Plugins:
|
||||||
|
* Logs (`/src/device-plugins/logs`)
|
||||||
|
* Layout inspector (`/src/plugins/layout`)
|
||||||
|
* Network inspector (`/src/plugins/network`)
|
||||||
|
* website and documentation (`/website` / `/docs`)
|
||||||
|
|
||||||
|
# Getting started
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
* macOS (while Sonar is buildable using other systems as well, only macOS is officially supported)
|
||||||
|
* node >= 8
|
||||||
|
* yarn >= 1.5
|
||||||
|
* iOS developer tools (for developing iOS plugins)
|
||||||
|
* Android SDK and adb
|
||||||
|
|
||||||
|
## Starting the desktop app
|
||||||
|
|
||||||
|
```
|
||||||
|
git clone https://github.com/facebook/Sonar.git
|
||||||
|
cd Sonar
|
||||||
|
yarn
|
||||||
|
yarn start
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building the desktop app
|
||||||
|
|
||||||
|
```
|
||||||
|
yarn build [macOnly] [build-number=$buildNumber]
|
||||||
|
```
|
||||||
|
|
||||||
|
A binary for macOS is created in `dist/mac`. `macOnly` and `build-number` are optional params.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
Find the full documentation for this project at [fbsonar.com](https://fbsonar.com/).
|
||||||
|
|
||||||
|
## Contributing and license
|
||||||
|
|
||||||
|
See the CONTRIBUTING file for how to help out.
|
||||||
|
Sonar is MIT licensed, as found in the LICENSE file.
|
||||||
4
android/AndroidManifest.xml
Normal file
4
android/AndroidManifest.xml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="com.facebook.sonar">
|
||||||
|
</manifest>
|
||||||
63
android/CMakeLists.txt
Normal file
63
android/CMakeLists.txt
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
cmake_minimum_required (VERSION 3.6.0)
|
||||||
|
project(sonar CXX C)
|
||||||
|
set(CMAKE_VERBOSE_MAKEFILE on)
|
||||||
|
|
||||||
|
set(PACKAGE_NAME "sonar")
|
||||||
|
|
||||||
|
add_compile_options(-DFOLLY_NO_CONFIG
|
||||||
|
-DSONAR_JNI_EXTERNAL=1
|
||||||
|
-DFB_SONARKIT_ENABLED=1
|
||||||
|
-DFOLLY_HAVE_MEMRCHR
|
||||||
|
-DFOLLY_MOBILE=1
|
||||||
|
-DFOLLY_USE_LIBCPP=1
|
||||||
|
-DFOLLY_HAVE_LIBJEMALLOC=0
|
||||||
|
-DFOLLY_HAVE_PREADV=0
|
||||||
|
-frtti
|
||||||
|
-fexceptions
|
||||||
|
-std=c++14
|
||||||
|
-Wno-error
|
||||||
|
-Wno-unused-local-typedefs
|
||||||
|
-Wno-unused-variable
|
||||||
|
-Wno-sign-compare
|
||||||
|
-Wno-comment
|
||||||
|
-Wno-return-type
|
||||||
|
-Wno-tautological-constant-compare
|
||||||
|
)
|
||||||
|
|
||||||
|
file(GLOB SOURCES android/sonar.cpp)
|
||||||
|
add_library(${PACKAGE_NAME} SHARED ${SOURCES})
|
||||||
|
target_include_directories(${PACKAGE_NAME} PUBLIC "./")
|
||||||
|
|
||||||
|
set(libjnihack_DIR ${CMAKE_SOURCE_DIR}/../libs/jni-hack/)
|
||||||
|
set(libfbjni_DIR ${CMAKE_SOURCE_DIR}/../libs/fbjni/src/main/cpp/include/)
|
||||||
|
set(libsonar_DIR ${CMAKE_SOURCE_DIR}/../xplat/)
|
||||||
|
set(third_party_ndk build/third-party-ndk)
|
||||||
|
set(libfolly_DIR ${third_party_ndk}/folly/)
|
||||||
|
set(glog_DIR ${third_party_ndk}/glog)
|
||||||
|
set(BOOST_DIR ${third_party_ndk}/boost/boost_1_63_0/)
|
||||||
|
|
||||||
|
|
||||||
|
set(build_DIR ${CMAKE_SOURCE_DIR}/build)
|
||||||
|
|
||||||
|
set(fbjni_build_DIR ${build_DIR}/fbjni/${ANDROID_ABI})
|
||||||
|
set(libsonar_build_DIR ${build_DIR}/libsonar/${ANDROID_ABI})
|
||||||
|
set(libfolly_build_DIR ${build_DIR}/libfolly/${ANDROID_ABI})
|
||||||
|
|
||||||
|
file(MAKE_DIRECTORY ${build_DIR})
|
||||||
|
|
||||||
|
add_subdirectory(${libsonar_DIR} ${libsonar_build_DIR})
|
||||||
|
add_subdirectory(${libfbjni_DIR}/../ ${fbjni_build_DIR})
|
||||||
|
|
||||||
|
target_include_directories(${PACKAGE_NAME} PRIVATE
|
||||||
|
${libjnihack_DIR}
|
||||||
|
${libfbjni_DIR}
|
||||||
|
${libsonar_DIR}
|
||||||
|
${libfolly_DIR}
|
||||||
|
${glog_DIR}
|
||||||
|
${glog_DIR}/../
|
||||||
|
${glog_DIR}/glog-0.3.5/src/
|
||||||
|
${BOOST_DIR}
|
||||||
|
${BOOST_DIR}/../
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(${PACKAGE_NAME} fb sonarcpp)
|
||||||
83
android/android/AndroidSonarClient.java
Normal file
83
android/android/AndroidSonarClient.java
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* 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.sonar.android;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.net.wifi.WifiInfo;
|
||||||
|
import android.net.wifi.WifiManager;
|
||||||
|
import android.os.Build;
|
||||||
|
import com.facebook.sonar.core.SonarClient;
|
||||||
|
|
||||||
|
public final class AndroidSonarClient {
|
||||||
|
private static boolean sIsInitialized = false;
|
||||||
|
private static SonarThread sSonarThread;
|
||||||
|
|
||||||
|
public static synchronized SonarClient getInstance(Context context) {
|
||||||
|
if (!sIsInitialized) {
|
||||||
|
sSonarThread = new SonarThread();
|
||||||
|
sSonarThread.start();
|
||||||
|
|
||||||
|
final Context app = context.getApplicationContext();
|
||||||
|
SonarClientImpl.init(
|
||||||
|
sSonarThread.getEventBase(),
|
||||||
|
getServerHost(app),
|
||||||
|
"Android",
|
||||||
|
getFriendlyDeviceName(),
|
||||||
|
getId(),
|
||||||
|
getRunningAppName(app),
|
||||||
|
getPackageName(app),
|
||||||
|
context.getFilesDir().getAbsolutePath());
|
||||||
|
sIsInitialized = true;
|
||||||
|
}
|
||||||
|
return SonarClientImpl.getInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean isRunningOnGenymotion() {
|
||||||
|
return Build.FINGERPRINT.contains("vbox");
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean isRunningOnStockEmulator() {
|
||||||
|
return Build.FINGERPRINT.contains("generic") && !Build.FINGERPRINT.contains("vbox");
|
||||||
|
}
|
||||||
|
|
||||||
|
static String getId() {
|
||||||
|
return Build.SERIAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String getFriendlyDeviceName() {
|
||||||
|
if (isRunningOnGenymotion()) {
|
||||||
|
// Genymotion already has a friendly name by default
|
||||||
|
return Build.MODEL;
|
||||||
|
} else {
|
||||||
|
return Build.MODEL + " - " + Build.VERSION.RELEASE + " - API " + Build.VERSION.SDK_INT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static String getServerHost(Context context) {
|
||||||
|
if (isRunningOnStockEmulator()) {
|
||||||
|
return "10.0.2.2";
|
||||||
|
} else if (isRunningOnGenymotion()) {
|
||||||
|
// This is hand-wavy but works on but ipv4 and ipv6 genymotion
|
||||||
|
final WifiManager wifi = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
|
||||||
|
final WifiInfo info = wifi.getConnectionInfo();
|
||||||
|
final int ip = info.getIpAddress();
|
||||||
|
return String.format("%d.%d.%d.2", (ip & 0xff), (ip >> 8 & 0xff), (ip >> 16 & 0xff));
|
||||||
|
} else {
|
||||||
|
// Running on physical device. Sonar desktop will run `adb reverse tcp:8088 tcp:8088`
|
||||||
|
return "localhost";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static String getRunningAppName(Context context) {
|
||||||
|
return context.getApplicationInfo().loadLabel(context.getPackageManager()).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
static String getPackageName(Context context) {
|
||||||
|
return context.getPackageName();
|
||||||
|
}
|
||||||
|
}
|
||||||
32
android/android/EventBase.java
Normal file
32
android/android/EventBase.java
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2004-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* 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.sonar.android;
|
||||||
|
|
||||||
|
import com.facebook.jni.HybridClassBase;
|
||||||
|
import com.facebook.proguard.annotations.DoNotStrip;
|
||||||
|
import com.facebook.soloader.SoLoader;
|
||||||
|
import com.facebook.sonar.BuildConfig;
|
||||||
|
|
||||||
|
@DoNotStrip
|
||||||
|
class EventBase extends HybridClassBase {
|
||||||
|
static {
|
||||||
|
if (BuildConfig.IS_INTERNAL_BUILD) {
|
||||||
|
SoLoader.loadLibrary("sonar");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EventBase() {
|
||||||
|
initHybrid();
|
||||||
|
}
|
||||||
|
|
||||||
|
@DoNotStrip
|
||||||
|
native void loopForever();
|
||||||
|
|
||||||
|
@DoNotStrip
|
||||||
|
private native void initHybrid();
|
||||||
|
}
|
||||||
57
android/android/SonarClientImpl.java
Normal file
57
android/android/SonarClientImpl.java
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* 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.sonar.android;
|
||||||
|
|
||||||
|
import com.facebook.jni.HybridData;
|
||||||
|
import com.facebook.proguard.annotations.DoNotStrip;
|
||||||
|
import com.facebook.soloader.SoLoader;
|
||||||
|
import com.facebook.sonar.BuildConfig;
|
||||||
|
import com.facebook.sonar.core.SonarClient;
|
||||||
|
import com.facebook.sonar.core.SonarPlugin;
|
||||||
|
|
||||||
|
@DoNotStrip
|
||||||
|
class SonarClientImpl implements SonarClient {
|
||||||
|
static {
|
||||||
|
if (BuildConfig.IS_INTERNAL_BUILD) {
|
||||||
|
SoLoader.loadLibrary("sonar");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final HybridData mHybridData;
|
||||||
|
|
||||||
|
private SonarClientImpl(HybridData hd) {
|
||||||
|
mHybridData = hd;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static native void init(
|
||||||
|
EventBase eventBase,
|
||||||
|
String host,
|
||||||
|
String os,
|
||||||
|
String device,
|
||||||
|
String deviceId,
|
||||||
|
String app,
|
||||||
|
String appId,
|
||||||
|
String privateAppDirectory);
|
||||||
|
|
||||||
|
public static native SonarClientImpl getInstance();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public native void addPlugin(SonarPlugin plugin);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public native <T extends SonarPlugin> T getPlugin(String id);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public native void removePlugin(SonarPlugin plugin);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public native void start();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public native void stop();
|
||||||
|
}
|
||||||
52
android/android/SonarConnectionImpl.java
Normal file
52
android/android/SonarConnectionImpl.java
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* 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.sonar.android;
|
||||||
|
|
||||||
|
import com.facebook.jni.HybridData;
|
||||||
|
import com.facebook.proguard.annotations.DoNotStrip;
|
||||||
|
import com.facebook.soloader.SoLoader;
|
||||||
|
import com.facebook.sonar.BuildConfig;
|
||||||
|
import com.facebook.sonar.core.SonarArray;
|
||||||
|
import com.facebook.sonar.core.SonarConnection;
|
||||||
|
import com.facebook.sonar.core.SonarObject;
|
||||||
|
import com.facebook.sonar.core.SonarReceiver;
|
||||||
|
|
||||||
|
@DoNotStrip
|
||||||
|
class SonarConnectionImpl implements SonarConnection {
|
||||||
|
static {
|
||||||
|
if (BuildConfig.IS_INTERNAL_BUILD) {
|
||||||
|
SoLoader.loadLibrary("sonar");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final HybridData mHybridData;
|
||||||
|
|
||||||
|
private SonarConnectionImpl(HybridData hd) {
|
||||||
|
mHybridData = hd;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void send(String method, SonarObject params) {
|
||||||
|
sendObject(method, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void send(String method, SonarArray params) {
|
||||||
|
sendArray(method, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
public native void sendObject(String method, SonarObject params);
|
||||||
|
|
||||||
|
public native void sendArray(String method, SonarArray params);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public native void reportError(Throwable throwable);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public native void receive(String method, SonarReceiver receiver);
|
||||||
|
}
|
||||||
53
android/android/SonarResponderImpl.java
Normal file
53
android/android/SonarResponderImpl.java
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* 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.sonar.android;
|
||||||
|
|
||||||
|
import com.facebook.jni.HybridData;
|
||||||
|
import com.facebook.proguard.annotations.DoNotStrip;
|
||||||
|
import com.facebook.soloader.SoLoader;
|
||||||
|
import com.facebook.sonar.BuildConfig;
|
||||||
|
import com.facebook.sonar.core.SonarArray;
|
||||||
|
import com.facebook.sonar.core.SonarObject;
|
||||||
|
import com.facebook.sonar.core.SonarResponder;
|
||||||
|
|
||||||
|
@DoNotStrip
|
||||||
|
class SonarResponderImpl implements SonarResponder {
|
||||||
|
static {
|
||||||
|
if (BuildConfig.IS_INTERNAL_BUILD) {
|
||||||
|
SoLoader.loadLibrary("sonar");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final HybridData mHybridData;
|
||||||
|
|
||||||
|
private SonarResponderImpl(HybridData hd) {
|
||||||
|
mHybridData = hd;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void success(SonarObject params) {
|
||||||
|
successObject(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void success(SonarArray params) {
|
||||||
|
successArray(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void success() {
|
||||||
|
successObject(new SonarObject.Builder().build());
|
||||||
|
}
|
||||||
|
|
||||||
|
public native void successObject(SonarObject response);
|
||||||
|
|
||||||
|
public native void successArray(SonarArray response);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public native void error(SonarObject response);
|
||||||
|
}
|
||||||
44
android/android/SonarThread.java
Normal file
44
android/android/SonarThread.java
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2004-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* 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.sonar.android;
|
||||||
|
|
||||||
|
import android.os.Process;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
class SonarThread extends Thread {
|
||||||
|
private @Nullable EventBase mEventBase;
|
||||||
|
|
||||||
|
SonarThread() {
|
||||||
|
super("SonarEventBaseThread");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
|
||||||
|
synchronized (this) {
|
||||||
|
try {
|
||||||
|
mEventBase = new EventBase();
|
||||||
|
} finally {
|
||||||
|
notifyAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mEventBase.loopForever();
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized EventBase getEventBase() {
|
||||||
|
while (mEventBase == null) {
|
||||||
|
try {
|
||||||
|
wait();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mEventBase;
|
||||||
|
}
|
||||||
|
}
|
||||||
305
android/android/sonar.cpp
Normal file
305
android/android/sonar.cpp
Normal file
@@ -0,0 +1,305 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the LICENSE
|
||||||
|
* file in the root directory of this source tree.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
#include <fb/fbjni.h>
|
||||||
|
|
||||||
|
#include <folly/json.h>
|
||||||
|
#include <folly/io/async/EventBase.h>
|
||||||
|
#include <folly/io/async/EventBaseManager.h>
|
||||||
|
|
||||||
|
#include <Sonar/SonarClient.h>
|
||||||
|
#include <Sonar/SonarWebSocket.h>
|
||||||
|
#include <Sonar/SonarConnection.h>
|
||||||
|
#include <Sonar/SonarResponder.h>
|
||||||
|
|
||||||
|
using namespace facebook;
|
||||||
|
using namespace facebook::sonar;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
class JEventBase : public jni::HybridClass<JEventBase> {
|
||||||
|
public:
|
||||||
|
constexpr static auto kJavaDescriptor = "Lcom/facebook/sonar/android/EventBase;";
|
||||||
|
|
||||||
|
static void registerNatives() {
|
||||||
|
registerHybrid({
|
||||||
|
makeNativeMethod("initHybrid", JEventBase::initHybrid),
|
||||||
|
makeNativeMethod("loopForever", JEventBase::loopForever),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
folly::EventBase* eventBase() {
|
||||||
|
return &eventBase_;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
friend HybridBase;
|
||||||
|
|
||||||
|
JEventBase() {}
|
||||||
|
|
||||||
|
void loopForever() {
|
||||||
|
folly::EventBaseManager::get()->setEventBase(&eventBase_, false);
|
||||||
|
eventBase_.loopForever();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void initHybrid(jni::alias_ref<jhybridobject> o) {
|
||||||
|
return setCxxInstance(o);
|
||||||
|
}
|
||||||
|
|
||||||
|
folly::EventBase eventBase_;
|
||||||
|
};
|
||||||
|
|
||||||
|
class JSonarObject : public jni::JavaClass<JSonarObject> {
|
||||||
|
public:
|
||||||
|
constexpr static auto kJavaDescriptor = "Lcom/facebook/sonar/core/SonarObject;";
|
||||||
|
|
||||||
|
static jni::local_ref<JSonarObject> create(const folly::dynamic& json) {
|
||||||
|
return newInstance(folly::toJson(json));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string toJsonString() {
|
||||||
|
static const auto method = javaClassStatic()->getMethod<std::string()>("toJsonString");
|
||||||
|
return method(self())->toStdString();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class JSonarArray : public jni::JavaClass<JSonarArray> {
|
||||||
|
public:
|
||||||
|
constexpr static auto kJavaDescriptor = "Lcom/facebook/sonar/core/SonarArray;";
|
||||||
|
|
||||||
|
static jni::local_ref<JSonarArray> create(const folly::dynamic& json) {
|
||||||
|
return newInstance(folly::toJson(json));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string toJsonString() {
|
||||||
|
static const auto method = javaClassStatic()->getMethod<std::string()>("toJsonString");
|
||||||
|
return method(self())->toStdString();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class JSonarResponder : public jni::JavaClass<JSonarResponder> {
|
||||||
|
public:
|
||||||
|
constexpr static auto kJavaDescriptor = "Lcom/facebook/sonar/core/SonarResponder;";
|
||||||
|
};
|
||||||
|
|
||||||
|
class JSonarResponderImpl : public jni::HybridClass<JSonarResponderImpl, JSonarResponder> {
|
||||||
|
public:
|
||||||
|
constexpr static auto kJavaDescriptor = "Lcom/facebook/sonar/android/SonarResponderImpl;";
|
||||||
|
|
||||||
|
static void registerNatives() {
|
||||||
|
registerHybrid({
|
||||||
|
makeNativeMethod("successObject", JSonarResponderImpl::successObject),
|
||||||
|
makeNativeMethod("successArray", JSonarResponderImpl::successArray),
|
||||||
|
makeNativeMethod("error", JSonarResponderImpl::error),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void successObject(jni::alias_ref<JSonarObject> json) {
|
||||||
|
_responder->success(json ? folly::parseJson(json->toJsonString()) : folly::dynamic::object());
|
||||||
|
}
|
||||||
|
|
||||||
|
void successArray(jni::alias_ref<JSonarArray> json) {
|
||||||
|
_responder->success(json ? folly::parseJson(json->toJsonString()) : folly::dynamic::object());
|
||||||
|
}
|
||||||
|
|
||||||
|
void error(jni::alias_ref<JSonarObject> json) {
|
||||||
|
_responder->error(json ? folly::parseJson(json->toJsonString()) : folly::dynamic::object());
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
friend HybridBase;
|
||||||
|
std::shared_ptr<SonarResponder> _responder;
|
||||||
|
|
||||||
|
JSonarResponderImpl(std::shared_ptr<SonarResponder> responder): _responder(std::move(responder)) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
class JSonarReceiver : public jni::JavaClass<JSonarReceiver> {
|
||||||
|
public:
|
||||||
|
constexpr static auto kJavaDescriptor = "Lcom/facebook/sonar/core/SonarReceiver;";
|
||||||
|
|
||||||
|
void receive(const folly::dynamic params, std::shared_ptr<SonarResponder> responder) const {
|
||||||
|
static const auto method = javaClassStatic()->getMethod<void(jni::alias_ref<JSonarObject::javaobject>, jni::alias_ref<JSonarResponder::javaobject>)>("onReceive");
|
||||||
|
method(self(), JSonarObject::create(std::move(params)), JSonarResponderImpl::newObjectCxxArgs(responder));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class JSonarConnection : public jni::JavaClass<JSonarConnection> {
|
||||||
|
public:
|
||||||
|
constexpr static auto kJavaDescriptor = "Lcom/facebook/sonar/core/SonarConnection;";
|
||||||
|
};
|
||||||
|
|
||||||
|
class JSonarConnectionImpl : public jni::HybridClass<JSonarConnectionImpl, JSonarConnection> {
|
||||||
|
public:
|
||||||
|
constexpr static auto kJavaDescriptor = "Lcom/facebook/sonar/android/SonarConnectionImpl;";
|
||||||
|
|
||||||
|
static void registerNatives() {
|
||||||
|
registerHybrid({
|
||||||
|
makeNativeMethod("sendObject", JSonarConnectionImpl::sendObject),
|
||||||
|
makeNativeMethod("sendArray", JSonarConnectionImpl::sendArray),
|
||||||
|
makeNativeMethod("reportError", JSonarConnectionImpl::reportError),
|
||||||
|
makeNativeMethod("receive", JSonarConnectionImpl::receive),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void sendObject(const std::string method, jni::alias_ref<JSonarObject> json) {
|
||||||
|
_connection->send(std::move(method), json ? folly::parseJson(json->toJsonString()) : folly::dynamic::object());
|
||||||
|
}
|
||||||
|
|
||||||
|
void sendArray(const std::string method, jni::alias_ref<JSonarArray> json) {
|
||||||
|
_connection->send(std::move(method), json ? folly::parseJson(json->toJsonString()) : folly::dynamic::object());
|
||||||
|
}
|
||||||
|
|
||||||
|
void reportError(jni::alias_ref<jni::JThrowable> throwable) {
|
||||||
|
_connection->error(throwable->toString(), throwable->getStackTrace()->toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
void receive(const std::string method, jni::alias_ref<JSonarReceiver> receiver) {
|
||||||
|
auto global = make_global(receiver);
|
||||||
|
_connection->receive(std::move(method), [global] (const folly::dynamic& params, std::unique_ptr<SonarResponder> responder) {
|
||||||
|
global->receive(params, std::move(responder));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
friend HybridBase;
|
||||||
|
std::shared_ptr<SonarConnection> _connection;
|
||||||
|
|
||||||
|
JSonarConnectionImpl(std::shared_ptr<SonarConnection> connection): _connection(std::move(connection)) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
class JSonarPlugin : public jni::JavaClass<JSonarPlugin> {
|
||||||
|
public:
|
||||||
|
constexpr static auto kJavaDescriptor = "Lcom/facebook/sonar/core/SonarPlugin;";
|
||||||
|
|
||||||
|
std::string identifier() const {
|
||||||
|
static const auto method = javaClassStatic()->getMethod<std::string()>("getId");
|
||||||
|
return method(self())->toStdString();
|
||||||
|
}
|
||||||
|
|
||||||
|
void didConnect(std::shared_ptr<SonarConnection> conn) {
|
||||||
|
static const auto method = javaClassStatic()->getMethod<void(jni::alias_ref<JSonarConnection::javaobject>)>("onConnect");
|
||||||
|
method(self(), JSonarConnectionImpl::newObjectCxxArgs(conn));
|
||||||
|
}
|
||||||
|
|
||||||
|
void didDisconnect() {
|
||||||
|
static const auto method = javaClassStatic()->getMethod<void()>("onDisconnect");
|
||||||
|
method(self());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class JSonarPluginWrapper : public SonarPlugin {
|
||||||
|
public:
|
||||||
|
jni::global_ref<JSonarPlugin> jplugin;
|
||||||
|
|
||||||
|
virtual std::string identifier() const override {
|
||||||
|
return jplugin->identifier();
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void didConnect(std::shared_ptr<SonarConnection> conn) override {
|
||||||
|
jplugin->didConnect(conn);
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void didDisconnect() override {
|
||||||
|
jplugin->didDisconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
JSonarPluginWrapper(jni::global_ref<JSonarPlugin> plugin): jplugin(plugin) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
class JSonarClient : public jni::HybridClass<JSonarClient> {
|
||||||
|
public:
|
||||||
|
constexpr static auto kJavaDescriptor = "Lcom/facebook/sonar/android/SonarClientImpl;";
|
||||||
|
|
||||||
|
static void registerNatives() {
|
||||||
|
registerHybrid({
|
||||||
|
makeNativeMethod("init", JSonarClient::init),
|
||||||
|
makeNativeMethod("getInstance", JSonarClient::getInstance),
|
||||||
|
makeNativeMethod("start", JSonarClient::start),
|
||||||
|
makeNativeMethod("stop", JSonarClient::stop),
|
||||||
|
makeNativeMethod("addPlugin", JSonarClient::addPlugin),
|
||||||
|
makeNativeMethod("removePlugin", JSonarClient::removePlugin),
|
||||||
|
makeNativeMethod("getPlugin", JSonarClient::getPlugin),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static jni::alias_ref<JSonarClient::javaobject> getInstance(jni::alias_ref<jclass>) {
|
||||||
|
static auto client = make_global(newObjectCxxArgs());
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
void start() {
|
||||||
|
SonarClient::instance()->start();
|
||||||
|
}
|
||||||
|
|
||||||
|
void stop() {
|
||||||
|
SonarClient::instance()->stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void addPlugin(jni::alias_ref<JSonarPlugin> plugin) {
|
||||||
|
auto wrapper = std::make_shared<JSonarPluginWrapper>(make_global(plugin));
|
||||||
|
SonarClient::instance()->addPlugin(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
void removePlugin(jni::alias_ref<JSonarPlugin> plugin) {
|
||||||
|
auto client = SonarClient::instance();
|
||||||
|
client->removePlugin(client->getPlugin(plugin->identifier()));
|
||||||
|
}
|
||||||
|
|
||||||
|
jni::alias_ref<JSonarPlugin> getPlugin(const std::string& identifier) {
|
||||||
|
auto plugin = SonarClient::instance()->getPlugin(identifier);
|
||||||
|
if (plugin) {
|
||||||
|
auto wrapper = std::static_pointer_cast<JSonarPluginWrapper>(plugin);
|
||||||
|
return wrapper->jplugin;
|
||||||
|
} else {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void init(
|
||||||
|
jni::alias_ref<jclass>,
|
||||||
|
JEventBase* jEventBase,
|
||||||
|
const std::string host,
|
||||||
|
const std::string os,
|
||||||
|
const std::string device,
|
||||||
|
const std::string deviceId,
|
||||||
|
const std::string app,
|
||||||
|
const std::string appId,
|
||||||
|
const std::string privateAppDirectory) {
|
||||||
|
|
||||||
|
SonarClient::init({
|
||||||
|
{
|
||||||
|
std::move(host),
|
||||||
|
std::move(os),
|
||||||
|
std::move(device),
|
||||||
|
std::move(deviceId),
|
||||||
|
std::move(app),
|
||||||
|
std::move(appId),
|
||||||
|
std::move(privateAppDirectory)
|
||||||
|
},
|
||||||
|
jEventBase->eventBase(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
friend HybridBase;
|
||||||
|
|
||||||
|
JSonarClient() {}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
jint JNI_OnLoad(JavaVM* vm, void*) {
|
||||||
|
return jni::initialize(vm, [] {
|
||||||
|
JSonarClient::registerNatives();
|
||||||
|
JSonarConnectionImpl::registerNatives();
|
||||||
|
JSonarResponderImpl::registerNatives();
|
||||||
|
JEventBase::registerNatives();
|
||||||
|
});
|
||||||
|
}
|
||||||
54
android/android/utils/SonarUtils.java
Normal file
54
android/android/utils/SonarUtils.java
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* 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.sonar.android.utils;
|
||||||
|
|
||||||
|
import android.app.ActivityManager;
|
||||||
|
import android.content.Context;
|
||||||
|
import com.facebook.sonar.BuildConfig;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public final class SonarUtils {
|
||||||
|
|
||||||
|
private SonarUtils() {}
|
||||||
|
|
||||||
|
public static boolean shouldEnableSonar(Context context) {
|
||||||
|
return BuildConfig.IS_INTERNAL_BUILD && !isEndToEndTest() && isMainProcess(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isEndToEndTest() {
|
||||||
|
final String value = System.getenv("BUDDY_SONAR_DISABLED");
|
||||||
|
if (value == null || value.length() == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return Boolean.parseBoolean(value);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isMainProcess(Context context) {
|
||||||
|
final int pid = android.os.Process.myPid();
|
||||||
|
final ActivityManager manager =
|
||||||
|
(ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
|
||||||
|
final List<ActivityManager.RunningAppProcessInfo> infoList = manager.getRunningAppProcesses();
|
||||||
|
|
||||||
|
String processName = null;
|
||||||
|
if (infoList != null) {
|
||||||
|
for (ActivityManager.RunningAppProcessInfo info : infoList) {
|
||||||
|
if (info.pid == pid) {
|
||||||
|
processName = info.processName;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return context.getPackageName().equals(processName);
|
||||||
|
}
|
||||||
|
}
|
||||||
172
android/build.gradle
Normal file
172
android/build.gradle
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
apply plugin: 'com.android.library'
|
||||||
|
|
||||||
|
apply plugin: 'de.undercouch.download'
|
||||||
|
|
||||||
|
import de.undercouch.gradle.tasks.download.Download
|
||||||
|
import org.apache.tools.ant.taskdefs.condition.Os
|
||||||
|
import org.apache.tools.ant.filters.ReplaceTokens
|
||||||
|
|
||||||
|
def downloadsDir = new File("$buildDir/downloads")
|
||||||
|
def thirdPartyNdkDir = new File("$buildDir/third-party-ndk")
|
||||||
|
|
||||||
|
task createNativeDepsDirectories {
|
||||||
|
downloadsDir.mkdirs()
|
||||||
|
thirdPartyNdkDir.mkdirs()
|
||||||
|
}
|
||||||
|
|
||||||
|
task downloadGlog(dependsOn: createNativeDepsDirectories, type: Download) {
|
||||||
|
src 'https://github.com/google/glog/archive/v0.3.5.tar.gz'
|
||||||
|
onlyIfNewer true
|
||||||
|
overwrite false
|
||||||
|
dest new File(downloadsDir, 'glog-0.3.5.tar.gz')
|
||||||
|
}
|
||||||
|
|
||||||
|
task prepareGlog(dependsOn: [downloadGlog], type: Copy) {
|
||||||
|
from tarTree(downloadGlog.dest)
|
||||||
|
from './third-party/glog/'
|
||||||
|
include 'glog-0.3.5/src/**/*', 'Android.mk', 'config.h', 'build.gradle', 'CMakeLists.txt', 'ApplicationManifest.xml'
|
||||||
|
includeEmptyDirs = false
|
||||||
|
filesMatching('**/*.h.in') {
|
||||||
|
filter(ReplaceTokens, tokens: [
|
||||||
|
ac_cv_have_unistd_h: '1',
|
||||||
|
ac_cv_have_stdint_h: '1',
|
||||||
|
ac_cv_have_systypes_h: '1',
|
||||||
|
ac_cv_have_inttypes_h: '1',
|
||||||
|
ac_cv_have_libgflags: '0',
|
||||||
|
ac_google_start_namespace: 'namespace google {',
|
||||||
|
ac_cv_have_uint16_t: '1',
|
||||||
|
ac_cv_have_u_int16_t: '1',
|
||||||
|
ac_cv_have___uint16: '0',
|
||||||
|
ac_google_end_namespace: '}',
|
||||||
|
ac_cv_have___builtin_expect: '1',
|
||||||
|
ac_google_namespace: 'google',
|
||||||
|
ac_cv___attribute___noinline: '__attribute__ ((noinline))',
|
||||||
|
ac_cv___attribute___noreturn: '__attribute__ ((noreturn))',
|
||||||
|
ac_cv___attribute___printf_4_5: '__attribute__((__format__ (__printf__, 4, 5)))'
|
||||||
|
])
|
||||||
|
it.path = (it.name - '.in')
|
||||||
|
}
|
||||||
|
into "$thirdPartyNdkDir/glog"
|
||||||
|
}
|
||||||
|
|
||||||
|
task finalizeGlog(dependsOn: [prepareGlog], type: Copy) {
|
||||||
|
from './third-party/glog/'
|
||||||
|
include 'logging.cc'
|
||||||
|
includeEmptyDirs = false
|
||||||
|
into "$thirdPartyNdkDir/glog/glog-0.3.5/src/"
|
||||||
|
}
|
||||||
|
|
||||||
|
task downloadDoubleConversion(dependsOn: createNativeDepsDirectories, type: Download) {
|
||||||
|
src 'https://github.com/google/double-conversion/archive/v3.0.0.tar.gz'
|
||||||
|
onlyIfNewer true
|
||||||
|
overwrite false
|
||||||
|
dest new File(downloadsDir, 'double-conversion-3.0.0.tar.gz')
|
||||||
|
}
|
||||||
|
|
||||||
|
task prepareDoubleConversion(dependsOn: [downloadDoubleConversion], type: Copy) {
|
||||||
|
from tarTree(downloadDoubleConversion.dest)
|
||||||
|
from './third-party/DoubleConversion/'
|
||||||
|
include 'double-conversion-3.0.0/**/*', 'build.gradle', 'CMakeLists.txt', 'ApplicationManifest.xml'
|
||||||
|
includeEmptyDirs = false
|
||||||
|
into "$thirdPartyNdkDir/double-conversion"
|
||||||
|
}
|
||||||
|
|
||||||
|
task downloadBoost(dependsOn: createNativeDepsDirectories, type: Download) {
|
||||||
|
src 'https://github.com/react-native-community/boost-for-react-native/releases/download/v1.63.0-0/boost_1_63_0.tar.gz'
|
||||||
|
onlyIfNewer true
|
||||||
|
overwrite false
|
||||||
|
dest new File(downloadsDir, 'boost_1_63_0.tar.gz')
|
||||||
|
}
|
||||||
|
|
||||||
|
task prepareBoost(dependsOn: [downloadBoost], type: Copy) {
|
||||||
|
from tarTree(resources.gzip(downloadBoost.dest))
|
||||||
|
include 'boost_1_63_0/boost/**/*.hpp', 'boost/boost/**/*.hpp'
|
||||||
|
includeEmptyDirs = false
|
||||||
|
into "$thirdPartyNdkDir/boost"
|
||||||
|
doLast {
|
||||||
|
file("$thirdPartyNdkDir/boost/boost").renameTo("$thirdPartyNdkDir/boost/boost_1_63_0")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
task downloadFolly(dependsOn: createNativeDepsDirectories, type: Download) {
|
||||||
|
src 'https://github.com/facebook/folly/archive/v2018.05.21.00.tar.gz'
|
||||||
|
onlyIfNewer true
|
||||||
|
overwrite false
|
||||||
|
dest new File(downloadsDir, 'folly-2018.05.21.00.tar.gz');
|
||||||
|
}
|
||||||
|
|
||||||
|
task prepareFolly(dependsOn: [downloadFolly], type: Copy) {
|
||||||
|
from tarTree(downloadFolly.dest)
|
||||||
|
from './third-party/folly/'
|
||||||
|
include 'folly-2018.05.21.00/folly/**/*', 'build.gradle', 'CMakeLists.txt', 'ApplicationManifest.xml'
|
||||||
|
eachFile {fname -> fname.path = (fname.path - "folly-2018.05.21.00/")}
|
||||||
|
includeEmptyDirs = false
|
||||||
|
into "$thirdPartyNdkDir/folly"
|
||||||
|
}
|
||||||
|
|
||||||
|
task prepareAllLibs() {
|
||||||
|
dependsOn finalizeGlog
|
||||||
|
dependsOn prepareDoubleConversion
|
||||||
|
dependsOn prepareBoost
|
||||||
|
dependsOn prepareFolly
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdkVersion rootProject.compileSdkVersion
|
||||||
|
buildToolsVersion rootProject.buildToolsVersion
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdkVersion rootProject.minSdkVersion
|
||||||
|
targetSdkVersion rootProject.targetSdkVersion
|
||||||
|
buildConfigField "boolean", "IS_INTERNAL_BUILD", 'true'
|
||||||
|
ndk {
|
||||||
|
abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'
|
||||||
|
}
|
||||||
|
|
||||||
|
externalNativeBuild {
|
||||||
|
cmake {
|
||||||
|
arguments '-DANDROID_TOOLCHAIN=clang'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lintOptions {
|
||||||
|
abortOnError false
|
||||||
|
}
|
||||||
|
sourceSets {
|
||||||
|
main {
|
||||||
|
manifest.srcFile './AndroidManifest.xml'
|
||||||
|
java {
|
||||||
|
srcDir 'android'
|
||||||
|
srcDir 'core'
|
||||||
|
srcDir 'plugins'
|
||||||
|
}
|
||||||
|
res {
|
||||||
|
srcDir 'res'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
externalNativeBuild {
|
||||||
|
cmake {
|
||||||
|
path './CMakeLists.txt'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation project(':fbjni')
|
||||||
|
implementation deps.soloader
|
||||||
|
compileOnly deps.lithoAnnotations
|
||||||
|
implementation 'org.glassfish:javax.annotation:10.0-b28'
|
||||||
|
implementation deps.guava
|
||||||
|
implementation deps.jsr305
|
||||||
|
implementation deps.supportAppCompat
|
||||||
|
implementation deps.stetho
|
||||||
|
implementation deps.okhttp3
|
||||||
|
implementation 'com.facebook.litho:litho-core:0.15.0'
|
||||||
|
implementation 'com.facebook.litho:litho-widget:0.15.0'
|
||||||
|
implementation fileTree(dir: 'plugins/console/dependencies', include: ['*.jar'])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
project.afterEvaluate {
|
||||||
|
preBuild.dependsOn prepareAllLibs
|
||||||
|
}
|
||||||
34
android/core/ErrorReportingRunnable.java
Normal file
34
android/core/ErrorReportingRunnable.java
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* 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.sonar.core;
|
||||||
|
|
||||||
|
public abstract class ErrorReportingRunnable implements Runnable {
|
||||||
|
|
||||||
|
private final SonarConnection mConnection;
|
||||||
|
|
||||||
|
public ErrorReportingRunnable(SonarConnection connection) {
|
||||||
|
mConnection = connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void run() {
|
||||||
|
try {
|
||||||
|
runOrThrow();
|
||||||
|
} catch (Exception e) {
|
||||||
|
if (mConnection != null) {
|
||||||
|
mConnection.reportError(e);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
doFinally();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void doFinally() {}
|
||||||
|
|
||||||
|
protected abstract void runOrThrow() throws Exception;
|
||||||
|
}
|
||||||
164
android/core/SonarArray.java
Normal file
164
android/core/SonarArray.java
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* 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.sonar.core;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import org.json.JSONArray;
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
public class SonarArray {
|
||||||
|
final JSONArray mJson;
|
||||||
|
|
||||||
|
SonarArray(JSONArray json) {
|
||||||
|
mJson = (json != null ? json : new JSONArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
SonarArray(String json) {
|
||||||
|
try {
|
||||||
|
mJson = new JSONArray(json);
|
||||||
|
} catch (JSONException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public SonarDynamic getDynamic(int index) {
|
||||||
|
return new SonarDynamic(mJson.opt(index));
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getString(int index) {
|
||||||
|
return mJson.optString(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getInt(int index) {
|
||||||
|
return mJson.optInt(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getLong(int index) {
|
||||||
|
return mJson.optLong(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getFloat(int index) {
|
||||||
|
return (float) mJson.optDouble(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getDouble(int index) {
|
||||||
|
return mJson.optDouble(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean getBoolean(int index) {
|
||||||
|
return mJson.optBoolean(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SonarObject getObject(int index) {
|
||||||
|
final Object o = mJson.opt(index);
|
||||||
|
return new SonarObject((JSONObject) o);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SonarArray getArray(int index) {
|
||||||
|
final Object o = mJson.opt(index);
|
||||||
|
return new SonarArray((JSONArray) o);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int length() {
|
||||||
|
return mJson.length();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> toStringList() {
|
||||||
|
final int length = length();
|
||||||
|
final List<String> list = new ArrayList<>(length);
|
||||||
|
for (int i = 0; i < length; i++) {
|
||||||
|
list.add(getString(i));
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String toJsonString() {
|
||||||
|
return toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return mJson.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
return mJson.toString().equals(o.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return mJson.hashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Builder {
|
||||||
|
private final JSONArray mJson;
|
||||||
|
|
||||||
|
public Builder() {
|
||||||
|
mJson = new JSONArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder put(String s) {
|
||||||
|
mJson.put(s);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder put(Integer i) {
|
||||||
|
mJson.put(i);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder put(Long l) {
|
||||||
|
mJson.put(l);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder put(Float f) {
|
||||||
|
mJson.put(Float.isNaN(f) ? null : f);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder put(Double d) {
|
||||||
|
mJson.put(Double.isNaN(d) ? null : d);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder put(Boolean b) {
|
||||||
|
mJson.put(b);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder put(SonarValue v) {
|
||||||
|
return put(v.toSonarObject());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder put(SonarArray a) {
|
||||||
|
mJson.put(a == null ? null : a.mJson);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder put(SonarArray.Builder b) {
|
||||||
|
return put(b.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder put(SonarObject o) {
|
||||||
|
mJson.put(o == null ? null : o.mJson);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder put(SonarObject.Builder b) {
|
||||||
|
return put(b.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
public SonarArray build() {
|
||||||
|
return new SonarArray(mJson);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
android/core/SonarClient.java
Normal file
20
android/core/SonarClient.java
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* 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.sonar.core;
|
||||||
|
|
||||||
|
public interface SonarClient {
|
||||||
|
void addPlugin(SonarPlugin plugin);
|
||||||
|
|
||||||
|
<T extends SonarPlugin> T getPlugin(String id);
|
||||||
|
|
||||||
|
void removePlugin(SonarPlugin plugin);
|
||||||
|
|
||||||
|
void start();
|
||||||
|
|
||||||
|
void stop();
|
||||||
|
}
|
||||||
37
android/core/SonarConnection.java
Normal file
37
android/core/SonarConnection.java
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* 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.sonar.core;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A connection between a SonarPlugin and the desktop Sonar application. Register request handlers
|
||||||
|
* to respond to calls made by the desktop application or directly send messages to the desktop
|
||||||
|
* application.
|
||||||
|
*/
|
||||||
|
public interface SonarConnection {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call a remote method on the Sonar desktop application, passing an optional JSON object as a
|
||||||
|
* parameter.
|
||||||
|
*/
|
||||||
|
void send(String method, SonarObject params);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call a remote method on the Sonar desktop application, passing an optional JSON array as a
|
||||||
|
* parameter.
|
||||||
|
*/
|
||||||
|
void send(String method, SonarArray params);
|
||||||
|
|
||||||
|
/** Report client error */
|
||||||
|
void reportError(Throwable throwable);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a receiver for a remote method call issued by the Sonar desktop application. The
|
||||||
|
* SonarReceiver is passed a responder to respond back to the desktop application.
|
||||||
|
*/
|
||||||
|
void receive(String method, SonarReceiver receiver);
|
||||||
|
}
|
||||||
124
android/core/SonarDynamic.java
Normal file
124
android/core/SonarDynamic.java
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* 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.sonar.core;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import org.json.JSONArray;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
public class SonarDynamic {
|
||||||
|
private final Object mObject;
|
||||||
|
|
||||||
|
public SonarDynamic(Object object) {
|
||||||
|
mObject = object;
|
||||||
|
}
|
||||||
|
|
||||||
|
public @Nullable String asString() {
|
||||||
|
if (mObject == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return mObject.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int asInt() {
|
||||||
|
if (mObject == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (mObject instanceof Integer) {
|
||||||
|
return (Integer) mObject;
|
||||||
|
}
|
||||||
|
if (mObject instanceof Long) {
|
||||||
|
return ((Long) mObject).intValue();
|
||||||
|
}
|
||||||
|
if (mObject instanceof Float) {
|
||||||
|
return ((Float) mObject).intValue();
|
||||||
|
}
|
||||||
|
if (mObject instanceof Double) {
|
||||||
|
return ((Double) mObject).intValue();
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long asLong() {
|
||||||
|
if (mObject == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (mObject instanceof Integer) {
|
||||||
|
return (Integer) mObject;
|
||||||
|
}
|
||||||
|
if (mObject instanceof Long) {
|
||||||
|
return (Long) mObject;
|
||||||
|
}
|
||||||
|
if (mObject instanceof Float) {
|
||||||
|
return ((Float) mObject).longValue();
|
||||||
|
}
|
||||||
|
if (mObject instanceof Double) {
|
||||||
|
return ((Double) mObject).longValue();
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float asFloat() {
|
||||||
|
if (mObject == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (mObject instanceof Integer) {
|
||||||
|
return (Integer) mObject;
|
||||||
|
}
|
||||||
|
if (mObject instanceof Long) {
|
||||||
|
return (Long) mObject;
|
||||||
|
}
|
||||||
|
if (mObject instanceof Float) {
|
||||||
|
return (Float) mObject;
|
||||||
|
}
|
||||||
|
if (mObject instanceof Double) {
|
||||||
|
return ((Double) mObject).floatValue();
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double asDouble() {
|
||||||
|
if (mObject == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (mObject instanceof Integer) {
|
||||||
|
return (Integer) mObject;
|
||||||
|
}
|
||||||
|
if (mObject instanceof Long) {
|
||||||
|
return (Long) mObject;
|
||||||
|
}
|
||||||
|
if (mObject instanceof Float) {
|
||||||
|
return (Float) mObject;
|
||||||
|
}
|
||||||
|
if (mObject instanceof Double) {
|
||||||
|
return (Double) mObject;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean asBoolean() {
|
||||||
|
if (mObject == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return (Boolean) mObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SonarObject asObject() {
|
||||||
|
if (mObject instanceof JSONObject) {
|
||||||
|
return new SonarObject((JSONObject) mObject);
|
||||||
|
}
|
||||||
|
return (SonarObject) mObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SonarArray asArray() {
|
||||||
|
if (mObject instanceof JSONArray) {
|
||||||
|
return new SonarArray((JSONArray) mObject);
|
||||||
|
}
|
||||||
|
return (SonarArray) mObject;
|
||||||
|
}
|
||||||
|
}
|
||||||
221
android/core/SonarObject.java
Normal file
221
android/core/SonarObject.java
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* 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.sonar.core;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import org.json.JSONArray;
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
public class SonarObject {
|
||||||
|
final JSONObject mJson;
|
||||||
|
|
||||||
|
public SonarObject(JSONObject json) {
|
||||||
|
mJson = (json != null ? json : new JSONObject());
|
||||||
|
}
|
||||||
|
|
||||||
|
public SonarObject(String json) {
|
||||||
|
try {
|
||||||
|
mJson = new JSONObject(json);
|
||||||
|
} catch (JSONException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public SonarDynamic getDynamic(String name) {
|
||||||
|
return new SonarDynamic(mJson.opt(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getString(String name) {
|
||||||
|
if (mJson.isNull(name)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return mJson.optString(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getInt(String name) {
|
||||||
|
return mJson.optInt(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getLong(String name) {
|
||||||
|
return mJson.optLong(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getFloat(String name) {
|
||||||
|
return (float) mJson.optDouble(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getDouble(String name) {
|
||||||
|
return mJson.optDouble(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean getBoolean(String name) {
|
||||||
|
return mJson.optBoolean(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SonarObject getObject(String name) {
|
||||||
|
final Object o = mJson.opt(name);
|
||||||
|
return new SonarObject((JSONObject) o);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SonarArray getArray(String name) {
|
||||||
|
final Object o = mJson.opt(name);
|
||||||
|
return new SonarArray((JSONArray) o);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean contains(String name) {
|
||||||
|
return mJson.has(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String toJsonString() {
|
||||||
|
return toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return mJson.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
return mJson.toString().equals(o.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return mJson.hashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Builder {
|
||||||
|
private final JSONObject mJson;
|
||||||
|
|
||||||
|
public Builder() {
|
||||||
|
mJson = new JSONObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder put(String name, Object obj) {
|
||||||
|
if (obj == null) {
|
||||||
|
return put(name, (String) null);
|
||||||
|
} else if (obj instanceof Integer) {
|
||||||
|
return put(name, (Integer) obj);
|
||||||
|
} else if (obj instanceof Long) {
|
||||||
|
return put(name, (Long) obj);
|
||||||
|
} else if (obj instanceof Float) {
|
||||||
|
return put(name, (Float) obj);
|
||||||
|
} else if (obj instanceof Double) {
|
||||||
|
return put(name, (Double) obj);
|
||||||
|
} else if (obj instanceof String) {
|
||||||
|
return put(name, (String) obj);
|
||||||
|
} else if (obj instanceof Boolean) {
|
||||||
|
return put(name, (Boolean) obj);
|
||||||
|
} else if (obj instanceof Object[]) {
|
||||||
|
return put(name, Arrays.deepToString((Object[]) obj));
|
||||||
|
} else if (obj instanceof SonarObject) {
|
||||||
|
return put(name, (SonarObject) obj);
|
||||||
|
} else if (obj instanceof SonarObject.Builder) {
|
||||||
|
return put(name, (SonarObject.Builder) obj);
|
||||||
|
} else if (obj instanceof SonarArray) {
|
||||||
|
return put(name, (SonarArray) obj);
|
||||||
|
} else if (obj instanceof SonarArray.Builder) {
|
||||||
|
return put(name, (SonarArray.Builder) obj);
|
||||||
|
} else if (obj instanceof SonarValue) {
|
||||||
|
return put(name, ((SonarValue) obj).toSonarObject());
|
||||||
|
} else {
|
||||||
|
return put(name, obj.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder put(String name, String s) {
|
||||||
|
try {
|
||||||
|
mJson.put(name, s);
|
||||||
|
} catch (JSONException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder put(String name, Integer i) {
|
||||||
|
try {
|
||||||
|
mJson.put(name, i);
|
||||||
|
} catch (JSONException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder put(String name, Long l) {
|
||||||
|
try {
|
||||||
|
mJson.put(name, l);
|
||||||
|
} catch (JSONException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder put(String name, Float f) {
|
||||||
|
try {
|
||||||
|
mJson.put(name, Float.isNaN(f) ? null : f);
|
||||||
|
} catch (JSONException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder put(String name, Double d) {
|
||||||
|
try {
|
||||||
|
mJson.put(name, Double.isNaN(d) ? null : d);
|
||||||
|
} catch (JSONException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder put(String name, Boolean b) {
|
||||||
|
try {
|
||||||
|
mJson.put(name, b);
|
||||||
|
} catch (JSONException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder put(String name, SonarValue v) {
|
||||||
|
return put(name, v.toSonarObject());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder put(String name, SonarArray a) {
|
||||||
|
try {
|
||||||
|
mJson.put(name, a == null ? null : a.mJson);
|
||||||
|
} catch (JSONException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder put(String name, SonarArray.Builder b) {
|
||||||
|
return put(name, b.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder put(String name, SonarObject o) {
|
||||||
|
try {
|
||||||
|
mJson.put(name, o == null ? null : o.mJson);
|
||||||
|
} catch (JSONException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder put(String name, SonarObject.Builder b) {
|
||||||
|
return put(name, b.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
public SonarObject build() {
|
||||||
|
return new SonarObject(mJson);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
37
android/core/SonarPlugin.java
Normal file
37
android/core/SonarPlugin.java
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* 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.sonar.core;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A SonarPlugin is an object which exposes an API to the Desktop Sonar application. When a
|
||||||
|
* connection is established the plugin is given a SonarConnection on which it can register request
|
||||||
|
* handlers and send messages. When the SonarConnection is invalid onDisconnect is called. onConnect
|
||||||
|
* may be called again on the same plugin object if Sonar re-connects, this will provide a new
|
||||||
|
* SonarConnection, do not attempt to re-use the previous connection.
|
||||||
|
*/
|
||||||
|
public interface SonarPlugin {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The id of this plugin. This is the namespace which Sonar desktop plugins will call
|
||||||
|
* methods on to route them to your plugin. This should match the id specified in your React
|
||||||
|
* plugin.
|
||||||
|
*/
|
||||||
|
String getId();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a connection has been established. The connection passed to this method is valid
|
||||||
|
* until {@link SonarPlugin#onDisconnect()} is called.
|
||||||
|
*/
|
||||||
|
void onConnect(SonarConnection connection) throws Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the connection passed to {@link SonarPlugin#onConnect(SonarConnection)} is no
|
||||||
|
* longer valid. Do not try to use the connection in or after this method has been called.
|
||||||
|
*/
|
||||||
|
void onDisconnect() throws Exception;
|
||||||
|
}
|
||||||
25
android/core/SonarReceiver.java
Normal file
25
android/core/SonarReceiver.java
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* 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.sonar.core;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A receiver of a remote method call issued by the Sonar desktop application. If the given
|
||||||
|
* responder is present it means the Sonar desktop application is expecting a response.
|
||||||
|
*/
|
||||||
|
public interface SonarReceiver {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reciver for a request sent from the Sonar desktop client.
|
||||||
|
*
|
||||||
|
* @param params Optional set of parameters sent with the request.
|
||||||
|
* @param responder Optional responder for request. Some requests don't warrant a response
|
||||||
|
* through. In this case the request should be made from the desktop using send() instead of
|
||||||
|
* call().
|
||||||
|
*/
|
||||||
|
void onReceive(SonarObject params, SonarResponder responder) throws Exception;
|
||||||
|
}
|
||||||
28
android/core/SonarResponder.java
Normal file
28
android/core/SonarResponder.java
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* 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.sonar.core;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SonarResponder is used to asyncronously response to a messaged recieved from the Sonar desktop
|
||||||
|
* app. The Sonar Responder will automatically wrap the response in an approriate object depending
|
||||||
|
* on if it is an error or not.
|
||||||
|
*/
|
||||||
|
public interface SonarResponder {
|
||||||
|
|
||||||
|
/** Deliver a successful response to the Sonar desktop app. */
|
||||||
|
void success(SonarObject response);
|
||||||
|
|
||||||
|
/** Deliver a successful response to the Sonar desktop app. */
|
||||||
|
void success(SonarArray response);
|
||||||
|
|
||||||
|
/** Deliver a successful response to the Sonar desktop app. */
|
||||||
|
void success();
|
||||||
|
|
||||||
|
/** Inform the Sonar desktop app of an error in handling the request. */
|
||||||
|
void error(SonarObject response);
|
||||||
|
}
|
||||||
12
android/core/SonarValue.java
Normal file
12
android/core/SonarValue.java
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* 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.sonar.core;
|
||||||
|
|
||||||
|
public interface SonarValue {
|
||||||
|
SonarObject toSonarObject();
|
||||||
|
}
|
||||||
75
android/plugins/common/BufferingSonarPlugin.java
Normal file
75
android/plugins/common/BufferingSonarPlugin.java
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* 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.sonar.plugins.common;
|
||||||
|
|
||||||
|
import com.facebook.sonar.core.SonarConnection;
|
||||||
|
import com.facebook.sonar.core.SonarObject;
|
||||||
|
import com.facebook.sonar.core.SonarPlugin;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sonar plugin that keeps events in a buffer until a connection is available.
|
||||||
|
*
|
||||||
|
* <p>In order to send data to the {@link SonarConnection}, use {@link #send(String, SonarObject)}
|
||||||
|
* instead of {@link SonarConnection#send(String, SonarObject)}.
|
||||||
|
*/
|
||||||
|
public abstract class BufferingSonarPlugin implements SonarPlugin {
|
||||||
|
|
||||||
|
private static final int BUFFER_SIZE = 500;
|
||||||
|
|
||||||
|
private @Nullable RingBuffer<CachedSonarEvent> mEventQueue;
|
||||||
|
private @Nullable SonarConnection mConnection;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void onConnect(SonarConnection connection) {
|
||||||
|
mConnection = connection;
|
||||||
|
|
||||||
|
sendBufferedEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void onDisconnect() {
|
||||||
|
mConnection = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized SonarConnection getConnection() {
|
||||||
|
return mConnection;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized boolean isConnected() {
|
||||||
|
return mConnection != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void send(String method, SonarObject sonarObject) {
|
||||||
|
if (mEventQueue == null) {
|
||||||
|
mEventQueue = new RingBuffer<>(BUFFER_SIZE);
|
||||||
|
}
|
||||||
|
mEventQueue.enqueue(new CachedSonarEvent(method, sonarObject));
|
||||||
|
if (mConnection != null) {
|
||||||
|
mConnection.send(method, sonarObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void sendBufferedEvents() {
|
||||||
|
if (mEventQueue != null && mConnection != null) {
|
||||||
|
for (CachedSonarEvent cachedSonarEvent : mEventQueue.asList()) {
|
||||||
|
mConnection.send(cachedSonarEvent.method, cachedSonarEvent.sonarObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class CachedSonarEvent {
|
||||||
|
final String method;
|
||||||
|
final SonarObject sonarObject;
|
||||||
|
|
||||||
|
private CachedSonarEvent(String method, SonarObject sonarObject) {
|
||||||
|
this.method = method;
|
||||||
|
this.sonarObject = sonarObject;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
40
android/plugins/common/MainThreadSonarReceiver.java
Normal file
40
android/plugins/common/MainThreadSonarReceiver.java
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* 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.sonar.plugins.common;
|
||||||
|
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import com.facebook.sonar.core.ErrorReportingRunnable;
|
||||||
|
import com.facebook.sonar.core.SonarConnection;
|
||||||
|
import com.facebook.sonar.core.SonarObject;
|
||||||
|
import com.facebook.sonar.core.SonarReceiver;
|
||||||
|
import com.facebook.sonar.core.SonarResponder;
|
||||||
|
|
||||||
|
public abstract class MainThreadSonarReceiver implements SonarReceiver {
|
||||||
|
|
||||||
|
public MainThreadSonarReceiver(SonarConnection connection) {
|
||||||
|
this.mConnection = connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final SonarConnection mConnection;
|
||||||
|
private final Handler mHandler = new Handler(Looper.getMainLooper());
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void onReceive(final SonarObject params, final SonarResponder responder) {
|
||||||
|
mHandler.post(
|
||||||
|
new ErrorReportingRunnable(mConnection) {
|
||||||
|
@Override
|
||||||
|
public void runOrThrow() throws Exception {
|
||||||
|
onReceiveOnMainThread(params, responder);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract void onReceiveOnMainThread(SonarObject params, SonarResponder responder)
|
||||||
|
throws Exception;
|
||||||
|
}
|
||||||
31
android/plugins/common/RingBuffer.java
Normal file
31
android/plugins/common/RingBuffer.java
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* 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.sonar.plugins.common;
|
||||||
|
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
final class RingBuffer<T> {
|
||||||
|
final int mBufferSize;
|
||||||
|
final List<T> mBuffer = new LinkedList<>();
|
||||||
|
|
||||||
|
RingBuffer(int bufferSize) {
|
||||||
|
mBufferSize = bufferSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
void enqueue(T item) {
|
||||||
|
if (mBuffer.size() >= mBufferSize) {
|
||||||
|
mBuffer.remove(0);
|
||||||
|
}
|
||||||
|
mBuffer.add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<T> asList() {
|
||||||
|
return mBuffer;
|
||||||
|
}
|
||||||
|
}
|
||||||
35
android/plugins/console/ConsoleSonarPlugin.java
Normal file
35
android/plugins/console/ConsoleSonarPlugin.java
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* 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.sonar.plugins.console;
|
||||||
|
|
||||||
|
import com.facebook.sonar.core.SonarConnection;
|
||||||
|
import com.facebook.sonar.core.SonarPlugin;
|
||||||
|
import com.facebook.sonar.plugins.console.iface.ConsoleCommandReceiver;
|
||||||
|
|
||||||
|
public class ConsoleSonarPlugin implements SonarPlugin {
|
||||||
|
|
||||||
|
private final JavascriptEnvironment mJavascriptEnvironment;
|
||||||
|
private JavascriptSession mJavascriptSession;
|
||||||
|
|
||||||
|
public ConsoleSonarPlugin(JavascriptEnvironment jsEnvironment) {
|
||||||
|
this.mJavascriptEnvironment = jsEnvironment;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return "Console";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onConnect(SonarConnection connection) throws Exception {
|
||||||
|
ConsoleCommandReceiver.listenForCommands(connection, mJavascriptEnvironment);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onDisconnect() throws Exception {
|
||||||
|
}
|
||||||
|
}
|
||||||
58
android/plugins/console/JavascriptEnvironment.java
Normal file
58
android/plugins/console/JavascriptEnvironment.java
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* 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.sonar.plugins.console;
|
||||||
|
|
||||||
|
import com.facebook.sonar.plugins.console.iface.ScriptingEnvironment;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import org.mozilla.javascript.Context;
|
||||||
|
import org.mozilla.javascript.ContextFactory;
|
||||||
|
|
||||||
|
public class JavascriptEnvironment implements ScriptingEnvironment {
|
||||||
|
|
||||||
|
private final Map<String, Object> mBoundVariables;
|
||||||
|
private final ContextFactory mContextFactory;
|
||||||
|
|
||||||
|
public JavascriptEnvironment() {
|
||||||
|
mBoundVariables = new HashMap<>();
|
||||||
|
mContextFactory =
|
||||||
|
new ContextFactory() {
|
||||||
|
@Override
|
||||||
|
public boolean hasFeature(Context cx, int featureIndex) {
|
||||||
|
return featureIndex == Context.FEATURE_ENHANCED_JAVA_ACCESS;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JavascriptSession startSession() {
|
||||||
|
return new JavascriptSession(mContextFactory, mBoundVariables);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method for other plugins to register objects to a name, so that they can be accessed in all
|
||||||
|
* console sessions.
|
||||||
|
*
|
||||||
|
* @param name The variable name to bind the object to.
|
||||||
|
* @param object The reference to bind.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void registerGlobalObject(String name, Object object) {
|
||||||
|
if (mBoundVariables.containsKey(name)) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
String.format(
|
||||||
|
"Variable %s is already reserved for %s", name, mBoundVariables.get(name)));
|
||||||
|
}
|
||||||
|
mBoundVariables.put(name, object);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isEnabled() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
169
android/plugins/console/JavascriptSession.java
Normal file
169
android/plugins/console/JavascriptSession.java
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* 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.sonar.plugins.console;
|
||||||
|
|
||||||
|
import com.facebook.sonar.plugins.console.iface.ScriptingSession;
|
||||||
|
import java.io.Closeable;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
import org.json.JSONTokener;
|
||||||
|
import org.mozilla.javascript.Context;
|
||||||
|
import org.mozilla.javascript.ContextFactory;
|
||||||
|
import org.mozilla.javascript.Function;
|
||||||
|
import org.mozilla.javascript.NativeJSON;
|
||||||
|
import org.mozilla.javascript.NativeJavaMethod;
|
||||||
|
import org.mozilla.javascript.NativeJavaObject;
|
||||||
|
import org.mozilla.javascript.Scriptable;
|
||||||
|
import org.mozilla.javascript.ScriptableObject;
|
||||||
|
import org.mozilla.javascript.Undefined;
|
||||||
|
|
||||||
|
public class JavascriptSession implements Closeable, ScriptingSession {
|
||||||
|
|
||||||
|
private static final String TYPE = "type";
|
||||||
|
private static final String VALUE = "value";
|
||||||
|
public static final String JSON = "json";
|
||||||
|
private final Context mContext;
|
||||||
|
private final ContextFactory mContextFactory;
|
||||||
|
private final Scriptable mScope;
|
||||||
|
private final AtomicInteger lineNumber = new AtomicInteger(0);
|
||||||
|
|
||||||
|
JavascriptSession(ContextFactory contextFactory, Map<String, Object> globals) {
|
||||||
|
mContextFactory = contextFactory;
|
||||||
|
mContext = contextFactory.enterContext();
|
||||||
|
|
||||||
|
// Interpreted mode, or it will produce Dalvik incompatible bytecode.
|
||||||
|
mContext.setOptimizationLevel(-1);
|
||||||
|
mScope = mContext.initStandardObjects();
|
||||||
|
|
||||||
|
for (Map.Entry<String, Object> entry : globals.entrySet()) {
|
||||||
|
final Object value = entry.getValue();
|
||||||
|
|
||||||
|
if (value instanceof Number || value instanceof String) {
|
||||||
|
ScriptableObject.putConstProperty(mScope, entry.getKey(), entry.getValue());
|
||||||
|
} else {
|
||||||
|
// Calling java methods in the VM produces objects wrapped in NativeJava*.
|
||||||
|
// So passing in wrapped objects keeps them consistent.
|
||||||
|
ScriptableObject.putConstProperty(
|
||||||
|
mScope,
|
||||||
|
entry.getKey(),
|
||||||
|
new NativeJavaObject(mScope, entry.getValue(), entry.getValue().getClass()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JSONObject evaluateCommand(String userScript) throws JSONException {
|
||||||
|
return evaluateCommand(userScript, mScope);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JSONObject evaluateCommand(String userScript, Object context) throws JSONException {
|
||||||
|
Scriptable scope = new NativeJavaObject(mScope, context, context.getClass());
|
||||||
|
return evaluateCommand(userScript, scope);
|
||||||
|
}
|
||||||
|
|
||||||
|
private JSONObject evaluateCommand(String command, Scriptable scope) throws JSONException {
|
||||||
|
try {
|
||||||
|
// This may be called by any thread, and contexts have to be entered in the current thread
|
||||||
|
// before being used, so enter/exit every time.
|
||||||
|
mContextFactory.enterContext();
|
||||||
|
return toJson(
|
||||||
|
mContext.evaluateString(
|
||||||
|
scope, command, "sonar-console", lineNumber.incrementAndGet(), null));
|
||||||
|
} finally {
|
||||||
|
Context.exit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private JSONObject toJson(Object result) throws JSONException {
|
||||||
|
|
||||||
|
if (result instanceof String) {
|
||||||
|
return new JSONObject().put(TYPE, JSON).put(VALUE, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result instanceof Class) {
|
||||||
|
return new JSONObject().put(TYPE, "class").put(VALUE, ((Class) result).getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result instanceof NativeJavaObject
|
||||||
|
&& ((NativeJavaObject) result).unwrap() instanceof String) {
|
||||||
|
return new JSONObject().put(TYPE, JSON).put(VALUE, ((NativeJavaObject) result).unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result instanceof NativeJavaObject
|
||||||
|
&& ((NativeJavaObject) result).unwrap() instanceof Class) {
|
||||||
|
return new JSONObject()
|
||||||
|
.put(TYPE, "class")
|
||||||
|
.put(VALUE, ((NativeJavaObject) result).unwrap().toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result instanceof NativeJavaObject) {
|
||||||
|
final JSONObject o = new JSONObject();
|
||||||
|
o.put("toString", ((NativeJavaObject) result).unwrap().toString());
|
||||||
|
for (Object id : ((NativeJavaObject) result).getIds()) {
|
||||||
|
if (id instanceof String) {
|
||||||
|
final String name = (String) id;
|
||||||
|
final Object value = ((NativeJavaObject) result).get(name, (NativeJavaObject) result);
|
||||||
|
if (value != null && value instanceof NativeJavaMethod) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final String valueString = value == null ? null : safeUnwrap(value).toString();
|
||||||
|
o.put(name, valueString);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new JSONObject().put(TYPE, "javaObject").put(VALUE, o);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result instanceof NativeJavaMethod) {
|
||||||
|
final JSONObject o = new JSONObject();
|
||||||
|
o.put(TYPE, "method");
|
||||||
|
o.put("name", ((NativeJavaMethod) result).getFunctionName());
|
||||||
|
return o;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result == null || result instanceof Undefined) {
|
||||||
|
return new JSONObject().put(TYPE, "null");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result instanceof Function) {
|
||||||
|
final JSONObject o = new JSONObject();
|
||||||
|
o.put(TYPE, "function");
|
||||||
|
o.put(VALUE, Context.toString(result));
|
||||||
|
return o;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result instanceof ScriptableObject) {
|
||||||
|
return new JSONObject()
|
||||||
|
.put(TYPE, JSON)
|
||||||
|
.put(
|
||||||
|
VALUE,
|
||||||
|
new JSONTokener(NativeJSON.stringify(mContext, mScope, result, null, null).toString())
|
||||||
|
.nextValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result instanceof Number) {
|
||||||
|
return new JSONObject().put(TYPE, JSON).put(VALUE, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new JSONObject().put(TYPE, "unknown").put(VALUE, result.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
Context.exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Object safeUnwrap(Object o) {
|
||||||
|
if (o instanceof NativeJavaObject) {
|
||||||
|
return ((NativeJavaObject) o).unwrap();
|
||||||
|
}
|
||||||
|
return o;
|
||||||
|
}
|
||||||
|
}
|
||||||
88
android/plugins/console/iface/ConsoleCommandReceiver.java
Normal file
88
android/plugins/console/iface/ConsoleCommandReceiver.java
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* 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.sonar.plugins.console.iface;
|
||||||
|
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import com.facebook.sonar.core.SonarConnection;
|
||||||
|
import com.facebook.sonar.core.SonarObject;
|
||||||
|
import com.facebook.sonar.core.SonarReceiver;
|
||||||
|
import com.facebook.sonar.core.SonarResponder;
|
||||||
|
import com.facebook.sonar.plugins.common.MainThreadSonarReceiver;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience class for adding console execution to a Sonar Plugin. Calling {@link
|
||||||
|
* ConsoleCommandReceiver#listenForCommands(SonarConnection, ScriptingEnvironment, ContextProvider)}
|
||||||
|
* will add the necessary listeners for responding to command execution calls.
|
||||||
|
*/
|
||||||
|
public class ConsoleCommandReceiver {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Incoming command execution calls may reference a context ID that means something to your
|
||||||
|
* plugin. Implement {@link ContextProvider} to provide a mapping from context ID to java object.
|
||||||
|
* This will allow your sonar plugin to control the execution context of the command.
|
||||||
|
*/
|
||||||
|
public interface ContextProvider {
|
||||||
|
@Nullable
|
||||||
|
Object getObjectForId(String id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void listenForCommands(
|
||||||
|
final SonarConnection connection,
|
||||||
|
final ScriptingEnvironment scriptingEnvironment,
|
||||||
|
final ContextProvider contextProvider) {
|
||||||
|
|
||||||
|
final ScriptingSession session = scriptingEnvironment.startSession();
|
||||||
|
final SonarReceiver executeCommandReceiver =
|
||||||
|
new MainThreadSonarReceiver(connection) {
|
||||||
|
@Override
|
||||||
|
public void onReceiveOnMainThread(SonarObject params, SonarResponder responder)
|
||||||
|
throws Exception {
|
||||||
|
final String command = params.getString("command");
|
||||||
|
final String contextObjectId = params.getString("context");
|
||||||
|
final Object contextObject = contextProvider.getObjectForId(contextObjectId);
|
||||||
|
try {
|
||||||
|
JSONObject o =
|
||||||
|
contextObject == null
|
||||||
|
? session.evaluateCommand(command)
|
||||||
|
: session.evaluateCommand(command, contextObject);
|
||||||
|
responder.success(new SonarObject(o));
|
||||||
|
} catch (Exception e) {
|
||||||
|
responder.error(new SonarObject.Builder().put("message", e.getMessage()).build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
final SonarReceiver isEnabledReceiver =
|
||||||
|
new SonarReceiver() {
|
||||||
|
@Override
|
||||||
|
public void onReceive(SonarObject params, SonarResponder responder) throws Exception {
|
||||||
|
responder.success(
|
||||||
|
new SonarObject.Builder()
|
||||||
|
.put("isEnabled", scriptingEnvironment.isEnabled())
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
connection.receive("executeCommand", executeCommandReceiver);
|
||||||
|
connection.receive("isConsoleEnabled", isEnabledReceiver);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void listenForCommands(
|
||||||
|
SonarConnection connection, ScriptingEnvironment scriptingEnvironment) {
|
||||||
|
listenForCommands(connection, scriptingEnvironment, nullContextProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final ContextProvider nullContextProvider =
|
||||||
|
new ContextProvider() {
|
||||||
|
@Override
|
||||||
|
@Nullable
|
||||||
|
public Object getObjectForId(String id) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
43
android/plugins/console/iface/NullScriptingEnvironment.java
Normal file
43
android/plugins/console/iface/NullScriptingEnvironment.java
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* 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.sonar.plugins.console.iface;
|
||||||
|
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
public class NullScriptingEnvironment implements ScriptingEnvironment {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ScriptingSession startSession() {
|
||||||
|
return new NoOpScriptingSession();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registerGlobalObject(String name, Object object) {}
|
||||||
|
|
||||||
|
static class NoOpScriptingSession implements ScriptingSession {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JSONObject evaluateCommand(String userScript) throws JSONException {
|
||||||
|
throw new UnsupportedOperationException("Console plugin not enabled in this app");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JSONObject evaluateCommand(String userScript, Object context) throws JSONException {
|
||||||
|
return evaluateCommand(userScript);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isEnabled() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
17
android/plugins/console/iface/ScriptingEnvironment.java
Normal file
17
android/plugins/console/iface/ScriptingEnvironment.java
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* 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.sonar.plugins.console.iface;
|
||||||
|
|
||||||
|
public interface ScriptingEnvironment {
|
||||||
|
|
||||||
|
ScriptingSession startSession();
|
||||||
|
|
||||||
|
void registerGlobalObject(String name, Object object);
|
||||||
|
|
||||||
|
boolean isEnabled();
|
||||||
|
}
|
||||||
20
android/plugins/console/iface/ScriptingSession.java
Normal file
20
android/plugins/console/iface/ScriptingSession.java
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* 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.sonar.plugins.console.iface;
|
||||||
|
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
public interface ScriptingSession {
|
||||||
|
|
||||||
|
JSONObject evaluateCommand(String userScript) throws JSONException;
|
||||||
|
|
||||||
|
JSONObject evaluateCommand(String userScript, Object context) throws JSONException;
|
||||||
|
|
||||||
|
void close();
|
||||||
|
}
|
||||||
128
android/plugins/inspector/ApplicationWrapper.java
Normal file
128
android/plugins/inspector/ApplicationWrapper.java
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* 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.sonar.plugins.inspector;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.app.Application;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.view.View;
|
||||||
|
import com.facebook.sonar.plugins.inspector.descriptors.utils.AndroidRootResolver;
|
||||||
|
import com.facebook.sonar.plugins.inspector.descriptors.utils.AndroidRootResolver.Root;
|
||||||
|
import java.lang.ref.WeakReference;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class ApplicationWrapper implements Application.ActivityLifecycleCallbacks {
|
||||||
|
|
||||||
|
public interface ActivityStackChangedListener {
|
||||||
|
void onActivityStackChanged(List<Activity> stack);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final Application mApplication;
|
||||||
|
private final AndroidRootResolver mAndroidRootsResolver;
|
||||||
|
private final List<WeakReference<Activity>> mActivities;
|
||||||
|
private final Handler mHandler;
|
||||||
|
private ActivityStackChangedListener mListener;
|
||||||
|
|
||||||
|
public ApplicationWrapper(Application application) {
|
||||||
|
mApplication = application;
|
||||||
|
mAndroidRootsResolver = new AndroidRootResolver();
|
||||||
|
mApplication.registerActivityLifecycleCallbacks(this);
|
||||||
|
mActivities = new ArrayList<>();
|
||||||
|
mHandler = new Handler(Looper.getMainLooper());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
|
||||||
|
mActivities.add(new WeakReference<>(activity));
|
||||||
|
notifyListener();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onActivityStarted(Activity activity) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onActivityResumed(Activity activity) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onActivityPaused(Activity activity) {
|
||||||
|
if (activity.isFinishing()) {
|
||||||
|
final Iterator<WeakReference<Activity>> activityIterator = mActivities.iterator();
|
||||||
|
|
||||||
|
while (activityIterator.hasNext()) {
|
||||||
|
if (activityIterator.next().get() == activity) {
|
||||||
|
activityIterator.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyListener();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onActivityStopped(Activity activity) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onActivitySaveInstanceState(Activity activity, Bundle outState) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onActivityDestroyed(Activity activity) {}
|
||||||
|
|
||||||
|
private void notifyListener() {
|
||||||
|
if (mListener != null) {
|
||||||
|
mListener.onActivityStackChanged(getActivityStack());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setListener(ActivityStackChangedListener listener) {
|
||||||
|
mListener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Application getApplication() {
|
||||||
|
return mApplication;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Activity> getActivityStack() {
|
||||||
|
final List<Activity> activities = new ArrayList<>(mActivities.size());
|
||||||
|
final Iterator<WeakReference<Activity>> activityIterator = mActivities.iterator();
|
||||||
|
|
||||||
|
while (activityIterator.hasNext()) {
|
||||||
|
final Activity activity = activityIterator.next().get();
|
||||||
|
if (activity == null) {
|
||||||
|
activityIterator.remove();
|
||||||
|
} else {
|
||||||
|
activities.add(activity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return activities;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<View> getViewRoots() {
|
||||||
|
final List<Root> roots = mAndroidRootsResolver.listActiveRoots();
|
||||||
|
if (roots == null) {
|
||||||
|
return Collections.EMPTY_LIST;
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<View> viewRoots = new ArrayList<>(roots.size());
|
||||||
|
for (Root root : roots) {
|
||||||
|
viewRoots.add(root.view);
|
||||||
|
}
|
||||||
|
|
||||||
|
return viewRoots;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void postDelayed(Runnable r, long delayMillis) {
|
||||||
|
mHandler.postDelayed(r, delayMillis);
|
||||||
|
}
|
||||||
|
}
|
||||||
175
android/plugins/inspector/BoundsDrawable.java
Normal file
175
android/plugins/inspector/BoundsDrawable.java
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* 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.sonar.plugins.inspector;
|
||||||
|
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.ColorFilter;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.PixelFormat;
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import android.graphics.Region;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.text.TextPaint;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
public class BoundsDrawable extends Drawable {
|
||||||
|
public static final int COLOR_HIGHLIGHT_CONTENT = 0x888875c5;
|
||||||
|
public static final int COLOR_HIGHLIGHT_PADDING = 0x889dd185;
|
||||||
|
public static final int COLOR_HIGHLIGHT_MARGIN = 0x88f7b77b;
|
||||||
|
private static @Nullable BoundsDrawable sInstance;
|
||||||
|
|
||||||
|
private final TextPaint mTextPaint;
|
||||||
|
private final Paint mMarginPaint;
|
||||||
|
private final Paint mPaddingPaint;
|
||||||
|
private final Paint mContentPaint;
|
||||||
|
private final Rect mWorkRect;
|
||||||
|
private final Rect mMarginBounds;
|
||||||
|
private final Rect mPaddingBounds;
|
||||||
|
private final Rect mContentBounds;
|
||||||
|
|
||||||
|
private final int mStrokeWidth;
|
||||||
|
private final float mAscentOffset;
|
||||||
|
private final float mDensity;
|
||||||
|
|
||||||
|
public static BoundsDrawable getInstance(
|
||||||
|
float density, Rect marginBounds, Rect paddingBounds, Rect contentBounds) {
|
||||||
|
final BoundsDrawable drawable = getInstance(density);
|
||||||
|
drawable.setBounds(marginBounds, paddingBounds, contentBounds);
|
||||||
|
return drawable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BoundsDrawable getInstance(float density) {
|
||||||
|
if (sInstance == null) {
|
||||||
|
sInstance = new BoundsDrawable(density);
|
||||||
|
}
|
||||||
|
return sInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
private BoundsDrawable(float density) {
|
||||||
|
mWorkRect = new Rect();
|
||||||
|
mMarginBounds = new Rect();
|
||||||
|
mPaddingBounds = new Rect();
|
||||||
|
mContentBounds = new Rect();
|
||||||
|
|
||||||
|
mDensity = density;
|
||||||
|
|
||||||
|
mTextPaint = new TextPaint();
|
||||||
|
mTextPaint.setAntiAlias(true);
|
||||||
|
mTextPaint.setTextAlign(Paint.Align.CENTER);
|
||||||
|
mTextPaint.setTextSize(dpToPx(8f));
|
||||||
|
mAscentOffset = -mTextPaint.ascent() / 2f;
|
||||||
|
mStrokeWidth = dpToPx(2f);
|
||||||
|
|
||||||
|
mPaddingPaint = new Paint();
|
||||||
|
mPaddingPaint.setStyle(Paint.Style.FILL);
|
||||||
|
mPaddingPaint.setColor(COLOR_HIGHLIGHT_PADDING);
|
||||||
|
|
||||||
|
mContentPaint = new Paint();
|
||||||
|
mContentPaint.setStyle(Paint.Style.FILL);
|
||||||
|
mContentPaint.setColor(COLOR_HIGHLIGHT_CONTENT);
|
||||||
|
|
||||||
|
mMarginPaint = new Paint();
|
||||||
|
mMarginPaint.setStyle(Paint.Style.FILL);
|
||||||
|
mMarginPaint.setColor(COLOR_HIGHLIGHT_MARGIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBounds(Rect marginBounds, Rect paddingBounds, Rect contentBounds) {
|
||||||
|
mMarginBounds.set(marginBounds);
|
||||||
|
mPaddingBounds.set(paddingBounds);
|
||||||
|
mContentBounds.set(contentBounds);
|
||||||
|
setBounds(marginBounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void draw(Canvas canvas) {
|
||||||
|
canvas.drawRect(mContentBounds, mContentPaint);
|
||||||
|
|
||||||
|
int saveCount = canvas.save();
|
||||||
|
canvas.clipRect(mContentBounds, Region.Op.DIFFERENCE);
|
||||||
|
canvas.drawRect(mPaddingBounds, mPaddingPaint);
|
||||||
|
canvas.restoreToCount(saveCount);
|
||||||
|
|
||||||
|
saveCount = canvas.save();
|
||||||
|
canvas.clipRect(mPaddingBounds, Region.Op.DIFFERENCE);
|
||||||
|
canvas.drawRect(mMarginBounds, mMarginPaint);
|
||||||
|
canvas.restoreToCount(saveCount);
|
||||||
|
|
||||||
|
drawBoundsDimensions(canvas, mContentBounds);
|
||||||
|
|
||||||
|
// Disabled for now since Sonar doesn't support options too well at this point in time.
|
||||||
|
// Once options are supported, we should re-enable the calls below
|
||||||
|
// drawCardinalDimensionsBetween(canvas, mContentBounds, mPaddingBounds);
|
||||||
|
// drawCardinalDimensionsBetween(canvas, mPaddingBounds, mMarginBounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setAlpha(int alpha) {
|
||||||
|
// No-op
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setColorFilter(ColorFilter colorFilter) {
|
||||||
|
// No-op
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getOpacity() {
|
||||||
|
return PixelFormat.TRANSLUCENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawCardinalDimensionsBetween(Canvas canvas, Rect inner, Rect outer) {
|
||||||
|
mWorkRect.set(inner);
|
||||||
|
mWorkRect.left = outer.left;
|
||||||
|
mWorkRect.right = inner.left;
|
||||||
|
drawBoundsDimension(canvas, mWorkRect, mWorkRect.width());
|
||||||
|
mWorkRect.left = inner.right;
|
||||||
|
mWorkRect.right = outer.right;
|
||||||
|
drawBoundsDimension(canvas, mWorkRect, mWorkRect.width());
|
||||||
|
mWorkRect.set(outer);
|
||||||
|
mWorkRect.bottom = inner.top;
|
||||||
|
drawBoundsDimension(canvas, mWorkRect, mWorkRect.height());
|
||||||
|
mWorkRect.bottom = outer.bottom;
|
||||||
|
mWorkRect.top = inner.bottom;
|
||||||
|
drawBoundsDimension(canvas, mWorkRect, mWorkRect.height());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawBoundsDimension(Canvas canvas, Rect bounds, int value) {
|
||||||
|
if (value <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int saveCount = canvas.save();
|
||||||
|
canvas.translate(bounds.centerX(), bounds.centerY());
|
||||||
|
drawOutlinedText(canvas, value + "px");
|
||||||
|
canvas.restoreToCount(saveCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawBoundsDimensions(Canvas canvas, Rect bounds) {
|
||||||
|
int saveCount = canvas.save();
|
||||||
|
canvas.translate(bounds.centerX(), bounds.centerY());
|
||||||
|
drawOutlinedText(canvas, bounds.width() + "px \u00D7 " + bounds.height() + "px");
|
||||||
|
canvas.restoreToCount(saveCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawOutlinedText(Canvas canvas, String text) {
|
||||||
|
mTextPaint.setColor(Color.BLACK);
|
||||||
|
mTextPaint.setStrokeWidth(mStrokeWidth);
|
||||||
|
mTextPaint.setStyle(Paint.Style.STROKE);
|
||||||
|
canvas.drawText(text, 0f, mAscentOffset, mTextPaint);
|
||||||
|
|
||||||
|
mTextPaint.setColor(Color.WHITE);
|
||||||
|
mTextPaint.setStrokeWidth(0f);
|
||||||
|
mTextPaint.setStyle(Paint.Style.FILL);
|
||||||
|
canvas.drawText(text, 0f, mAscentOffset, mTextPaint);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int dpToPx(float dp) {
|
||||||
|
return (int) (dp * mDensity);
|
||||||
|
}
|
||||||
|
}
|
||||||
89
android/plugins/inspector/DescriptorMapping.java
Normal file
89
android/plugins/inspector/DescriptorMapping.java
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* 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.sonar.plugins.inspector;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.app.Dialog;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.Window;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import com.facebook.sonar.core.SonarConnection;
|
||||||
|
import com.facebook.sonar.plugins.inspector.descriptors.ActivityDescriptor;
|
||||||
|
import com.facebook.sonar.plugins.inspector.descriptors.ApplicationDescriptor;
|
||||||
|
import com.facebook.sonar.plugins.inspector.descriptors.DialogDescriptor;
|
||||||
|
import com.facebook.sonar.plugins.inspector.descriptors.DialogFragmentDescriptor;
|
||||||
|
import com.facebook.sonar.plugins.inspector.descriptors.DrawableDescriptor;
|
||||||
|
import com.facebook.sonar.plugins.inspector.descriptors.FragmentDescriptor;
|
||||||
|
import com.facebook.sonar.plugins.inspector.descriptors.ObjectDescriptor;
|
||||||
|
import com.facebook.sonar.plugins.inspector.descriptors.SupportDialogFragmentDescriptor;
|
||||||
|
import com.facebook.sonar.plugins.inspector.descriptors.SupportFragmentDescriptor;
|
||||||
|
import com.facebook.sonar.plugins.inspector.descriptors.TextViewDescriptor;
|
||||||
|
import com.facebook.sonar.plugins.inspector.descriptors.ViewDescriptor;
|
||||||
|
import com.facebook.sonar.plugins.inspector.descriptors.ViewGroupDescriptor;
|
||||||
|
import com.facebook.sonar.plugins.inspector.descriptors.WindowDescriptor;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A mapping from classes to the object use to describe instances of a class. When looking for a
|
||||||
|
* descriptor to describe an object this classs will traverse the object's class hierarchy until it
|
||||||
|
* finds a matching descriptor instance.
|
||||||
|
*/
|
||||||
|
public class DescriptorMapping {
|
||||||
|
private Map<Class<?>, NodeDescriptor<?>> mMapping = new HashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return A DescriptorMapping initialized with default descriptors for java and Android classes.
|
||||||
|
*/
|
||||||
|
public static DescriptorMapping withDefaults() {
|
||||||
|
final DescriptorMapping mapping = new DescriptorMapping();
|
||||||
|
mapping.register(Object.class, new ObjectDescriptor());
|
||||||
|
mapping.register(ApplicationWrapper.class, new ApplicationDescriptor());
|
||||||
|
mapping.register(Activity.class, new ActivityDescriptor());
|
||||||
|
mapping.register(Window.class, new WindowDescriptor());
|
||||||
|
mapping.register(ViewGroup.class, new ViewGroupDescriptor());
|
||||||
|
mapping.register(View.class, new ViewDescriptor());
|
||||||
|
mapping.register(TextView.class, new TextViewDescriptor());
|
||||||
|
mapping.register(Drawable.class, new DrawableDescriptor());
|
||||||
|
mapping.register(Dialog.class, new DialogDescriptor());
|
||||||
|
mapping.register(android.app.Fragment.class, new FragmentDescriptor());
|
||||||
|
mapping.register(android.support.v4.app.Fragment.class, new SupportFragmentDescriptor());
|
||||||
|
mapping.register(android.app.DialogFragment.class, new DialogFragmentDescriptor());
|
||||||
|
mapping.register(
|
||||||
|
android.support.v4.app.DialogFragment.class, new SupportDialogFragmentDescriptor());
|
||||||
|
return mapping;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Register a descriptor for a given class. */
|
||||||
|
public <T> void register(Class<T> clazz, NodeDescriptor<T> descriptor) {
|
||||||
|
mMapping.put(clazz, descriptor);
|
||||||
|
}
|
||||||
|
|
||||||
|
NodeDescriptor<?> descriptorForClass(Class<?> clazz) {
|
||||||
|
while (!mMapping.containsKey(clazz)) {
|
||||||
|
clazz = clazz.getSuperclass();
|
||||||
|
}
|
||||||
|
return mMapping.get(clazz);
|
||||||
|
}
|
||||||
|
|
||||||
|
void onConnect(SonarConnection connection) {
|
||||||
|
for (NodeDescriptor descriptor : mMapping.values()) {
|
||||||
|
descriptor.setConnection(connection);
|
||||||
|
descriptor.setDescriptorMapping(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onDisconnect() {
|
||||||
|
for (NodeDescriptor descriptor : mMapping.values()) {
|
||||||
|
descriptor.setConnection(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
android/plugins/inspector/HiddenNode.java
Normal file
12
android/plugins/inspector/HiddenNode.java
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* 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.sonar.plugins.inspector;
|
||||||
|
|
||||||
|
/** Marker interface to identify nodes which should not be traversed. */
|
||||||
|
public interface HiddenNode {}
|
||||||
69
android/plugins/inspector/HighlightedOverlay.java
Normal file
69
android/plugins/inspector/HighlightedOverlay.java
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* 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.sonar.plugins.inspector;
|
||||||
|
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A singleton instance of a overlay drawable used for highlighting node bounds. See {@link
|
||||||
|
* NodeDescriptor#setHighlighted(Object, boolean)}.
|
||||||
|
*/
|
||||||
|
public class HighlightedOverlay {
|
||||||
|
private static final boolean VIEW_OVERLAY_SUPPORT =
|
||||||
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Highlights a particular view with its content bounds, padding and margin dimensions
|
||||||
|
*
|
||||||
|
* @param targetView The view to apply the highlight on
|
||||||
|
* @param margin A {@link Rect} containing the margin values
|
||||||
|
* @param padding A {@link Rect} containing the padding values
|
||||||
|
* @param contentBounds The {@link Rect} bounds of the content, which includes padding
|
||||||
|
*/
|
||||||
|
public static void setHighlighted(
|
||||||
|
View targetView, Rect margin, Rect padding, Rect contentBounds) {
|
||||||
|
if (!VIEW_OVERLAY_SUPPORT) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
contentBounds.set(
|
||||||
|
contentBounds.left + padding.left,
|
||||||
|
contentBounds.top + padding.top,
|
||||||
|
contentBounds.right - padding.right,
|
||||||
|
contentBounds.bottom - padding.bottom);
|
||||||
|
|
||||||
|
padding = enclose(padding, contentBounds);
|
||||||
|
margin = enclose(margin, padding);
|
||||||
|
|
||||||
|
final float density = targetView.getContext().getResources().getDisplayMetrics().density;
|
||||||
|
final Drawable overlay = BoundsDrawable.getInstance(density, margin, padding, contentBounds);
|
||||||
|
targetView.getOverlay().add(overlay);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void removeHighlight(View targetView) {
|
||||||
|
if (!VIEW_OVERLAY_SUPPORT) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final float density = targetView.getContext().getResources().getDisplayMetrics().density;
|
||||||
|
final Drawable overlay = BoundsDrawable.getInstance(density);
|
||||||
|
targetView.getOverlay().remove(overlay);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Rect enclose(Rect parent, Rect child) {
|
||||||
|
return new Rect(
|
||||||
|
child.left - parent.left,
|
||||||
|
child.top - parent.top,
|
||||||
|
child.right + parent.right,
|
||||||
|
child.bottom + parent.bottom);
|
||||||
|
}
|
||||||
|
}
|
||||||
418
android/plugins/inspector/InspectorSonarPlugin.java
Normal file
418
android/plugins/inspector/InspectorSonarPlugin.java
Normal file
@@ -0,0 +1,418 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* 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.sonar.plugins.inspector;
|
||||||
|
|
||||||
|
import android.app.Application;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import com.facebook.sonar.core.ErrorReportingRunnable;
|
||||||
|
import com.facebook.sonar.core.SonarArray;
|
||||||
|
import com.facebook.sonar.core.SonarConnection;
|
||||||
|
import com.facebook.sonar.core.SonarDynamic;
|
||||||
|
import com.facebook.sonar.core.SonarObject;
|
||||||
|
import com.facebook.sonar.core.SonarPlugin;
|
||||||
|
import com.facebook.sonar.core.SonarReceiver;
|
||||||
|
import com.facebook.sonar.core.SonarResponder;
|
||||||
|
import com.facebook.sonar.plugins.common.MainThreadSonarReceiver;
|
||||||
|
import com.facebook.sonar.plugins.console.iface.ConsoleCommandReceiver;
|
||||||
|
import com.facebook.sonar.plugins.console.iface.NullScriptingEnvironment;
|
||||||
|
import com.facebook.sonar.plugins.console.iface.ScriptingEnvironment;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
public class InspectorSonarPlugin implements SonarPlugin {
|
||||||
|
|
||||||
|
private ApplicationWrapper mApplication;
|
||||||
|
private DescriptorMapping mDescriptorMapping;
|
||||||
|
private ObjectTracker mObjectTracker;
|
||||||
|
private ScriptingEnvironment mScriptingEnvironment;
|
||||||
|
private String mHighlightedId;
|
||||||
|
private TouchOverlayView mTouchOverlay;
|
||||||
|
private SonarConnection mConnection;
|
||||||
|
|
||||||
|
public InspectorSonarPlugin(
|
||||||
|
Context context,
|
||||||
|
DescriptorMapping descriptorMapping,
|
||||||
|
ScriptingEnvironment scriptingEnvironment) {
|
||||||
|
this(
|
||||||
|
new ApplicationWrapper((Application) context.getApplicationContext()),
|
||||||
|
descriptorMapping,
|
||||||
|
scriptingEnvironment);
|
||||||
|
}
|
||||||
|
|
||||||
|
public InspectorSonarPlugin(Context context, DescriptorMapping descriptorMapping) {
|
||||||
|
this(context, descriptorMapping, new NullScriptingEnvironment());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Package visible for testing
|
||||||
|
InspectorSonarPlugin(
|
||||||
|
ApplicationWrapper wrapper,
|
||||||
|
DescriptorMapping descriptorMapping,
|
||||||
|
ScriptingEnvironment scriptingEnvironment) {
|
||||||
|
mDescriptorMapping = descriptorMapping;
|
||||||
|
|
||||||
|
mObjectTracker = new ObjectTracker();
|
||||||
|
mApplication = wrapper;
|
||||||
|
mScriptingEnvironment = scriptingEnvironment;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return "Inspector";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onConnect(SonarConnection connection) throws Exception {
|
||||||
|
mConnection = connection;
|
||||||
|
mDescriptorMapping.onConnect(connection);
|
||||||
|
|
||||||
|
ConsoleCommandReceiver.listenForCommands(
|
||||||
|
connection,
|
||||||
|
mScriptingEnvironment,
|
||||||
|
new ConsoleCommandReceiver.ContextProvider() {
|
||||||
|
@Override
|
||||||
|
@Nullable
|
||||||
|
public Object getObjectForId(String id) {
|
||||||
|
return mObjectTracker.get(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
connection.receive("getRoot", mGetRoot);
|
||||||
|
connection.receive("getNodes", mGetNodes);
|
||||||
|
connection.receive("setData", mSetData);
|
||||||
|
connection.receive("setHighlighted", mSetHighlighted);
|
||||||
|
connection.receive("setSearchActive", mSetSearchActive);
|
||||||
|
connection.receive("getSearchResults", mGetSearchResults);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDisconnect() throws Exception {
|
||||||
|
if (mHighlightedId != null) {
|
||||||
|
setHighlighted(mHighlightedId, false);
|
||||||
|
mHighlightedId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
mObjectTracker.clear();
|
||||||
|
mDescriptorMapping.onDisconnect();
|
||||||
|
mConnection = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final SonarReceiver mGetRoot =
|
||||||
|
new MainThreadSonarReceiver(mConnection) {
|
||||||
|
@Override
|
||||||
|
public void onReceiveOnMainThread(SonarObject params, SonarResponder responder)
|
||||||
|
throws Exception {
|
||||||
|
responder.success(getNode(trackObject(mApplication)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
final SonarReceiver mGetNodes =
|
||||||
|
new MainThreadSonarReceiver(mConnection) {
|
||||||
|
@Override
|
||||||
|
public void onReceiveOnMainThread(final SonarObject params, final SonarResponder responder)
|
||||||
|
throws Exception {
|
||||||
|
final SonarArray ids = params.getArray("ids");
|
||||||
|
final SonarArray.Builder result = new SonarArray.Builder();
|
||||||
|
|
||||||
|
for (int i = 0, count = ids.length(); i < count; i++) {
|
||||||
|
final String id = ids.getString(i);
|
||||||
|
final SonarObject node = getNode(id);
|
||||||
|
if (node != null) {
|
||||||
|
result.put(node);
|
||||||
|
} else {
|
||||||
|
responder.error(
|
||||||
|
new SonarObject.Builder()
|
||||||
|
.put("message", "No node with given id")
|
||||||
|
.put("id", id)
|
||||||
|
.build());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
responder.success(new SonarObject.Builder().put("elements", result).build());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
final SonarReceiver mSetData =
|
||||||
|
new MainThreadSonarReceiver(mConnection) {
|
||||||
|
@Override
|
||||||
|
public void onReceiveOnMainThread(final SonarObject params, SonarResponder responder)
|
||||||
|
throws Exception {
|
||||||
|
final String nodeId = params.getString("id");
|
||||||
|
final SonarArray keyPath = params.getArray("path");
|
||||||
|
final SonarDynamic value = params.getDynamic("value");
|
||||||
|
|
||||||
|
final Object obj = mObjectTracker.get(nodeId);
|
||||||
|
if (obj == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final NodeDescriptor<Object> descriptor = descriptorForObject(obj);
|
||||||
|
if (descriptor == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int count = keyPath.length();
|
||||||
|
final String[] path = new String[count];
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
path[i] = keyPath.getString(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
descriptor.setValue(obj, path, value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
final SonarReceiver mSetHighlighted =
|
||||||
|
new MainThreadSonarReceiver(mConnection) {
|
||||||
|
@Override
|
||||||
|
public void onReceiveOnMainThread(final SonarObject params, SonarResponder responder)
|
||||||
|
throws Exception {
|
||||||
|
final String nodeId = params.getString("id");
|
||||||
|
|
||||||
|
if (mHighlightedId != null) {
|
||||||
|
setHighlighted(mHighlightedId, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nodeId != null) {
|
||||||
|
setHighlighted(nodeId, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
mHighlightedId = nodeId;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
final SonarReceiver mSetSearchActive =
|
||||||
|
new MainThreadSonarReceiver(mConnection) {
|
||||||
|
@Override
|
||||||
|
public void onReceiveOnMainThread(final SonarObject params, SonarResponder responder)
|
||||||
|
throws Exception {
|
||||||
|
final boolean active = params.getBoolean("active");
|
||||||
|
final List<View> roots = mApplication.getViewRoots();
|
||||||
|
|
||||||
|
ViewGroup root = null;
|
||||||
|
for (int i = roots.size() - 1; i >= 0; i--) {
|
||||||
|
if (roots.get(i) instanceof ViewGroup) {
|
||||||
|
root = (ViewGroup) roots.get(i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (root != null) {
|
||||||
|
if (active) {
|
||||||
|
mTouchOverlay = new TouchOverlayView(root.getContext());
|
||||||
|
root.addView(mTouchOverlay);
|
||||||
|
root.bringChildToFront(mTouchOverlay);
|
||||||
|
} else {
|
||||||
|
root.removeView(mTouchOverlay);
|
||||||
|
mTouchOverlay = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
final SonarReceiver mGetSearchResults =
|
||||||
|
new MainThreadSonarReceiver(mConnection) {
|
||||||
|
@Override
|
||||||
|
public void onReceiveOnMainThread(SonarObject params, SonarResponder responder)
|
||||||
|
throws Exception {
|
||||||
|
final String query = params.getString("query");
|
||||||
|
final SearchResultNode matchTree = searchTree(query.toLowerCase(), mApplication);
|
||||||
|
final SonarObject results = matchTree == null ? null : matchTree.toSonarObject();
|
||||||
|
final SonarObject response =
|
||||||
|
new SonarObject.Builder().put("results", results).put("query", query).build();
|
||||||
|
responder.success(response);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class TouchOverlayView extends View implements HiddenNode {
|
||||||
|
public TouchOverlayView(Context context) {
|
||||||
|
super(context);
|
||||||
|
setBackgroundColor(BoundsDrawable.COLOR_HIGHLIGHT_CONTENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onTouchEvent(final MotionEvent event) {
|
||||||
|
if (event.getAction() != MotionEvent.ACTION_UP) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
new ErrorReportingRunnable(mConnection) {
|
||||||
|
@Override
|
||||||
|
public void runOrThrow() throws Exception {
|
||||||
|
hitTest((int) event.getX(), (int) event.getY());
|
||||||
|
}
|
||||||
|
}.run();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void hitTest(final int touchX, final int touchY) throws Exception {
|
||||||
|
final SonarArray.Builder path = new SonarArray.Builder();
|
||||||
|
path.put(trackObject(mApplication));
|
||||||
|
|
||||||
|
final Touch touch =
|
||||||
|
new Touch() {
|
||||||
|
int x = touchX;
|
||||||
|
int y = touchY;
|
||||||
|
Object node = mApplication;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void finish() {
|
||||||
|
mConnection.send("select", new SonarObject.Builder().put("path", path).build());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void continueWithOffset(
|
||||||
|
final int childIndex, final int offsetX, final int offsetY) {
|
||||||
|
final Touch touch = this;
|
||||||
|
|
||||||
|
new ErrorReportingRunnable(mConnection) {
|
||||||
|
@Override
|
||||||
|
protected void runOrThrow() throws Exception {
|
||||||
|
x -= offsetX;
|
||||||
|
y -= offsetY;
|
||||||
|
|
||||||
|
node = assertNotNull(descriptorForObject(node).getChildAt(node, childIndex));
|
||||||
|
path.put(trackObject(node));
|
||||||
|
|
||||||
|
final NodeDescriptor<Object> descriptor = descriptorForObject(node);
|
||||||
|
descriptor.hitTest(node, touch);
|
||||||
|
}
|
||||||
|
}.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean containedIn(int l, int t, int r, int b) {
|
||||||
|
return x >= l && x <= r && y >= t && y <= b;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
final NodeDescriptor<Object> descriptor = descriptorForObject(mApplication);
|
||||||
|
descriptor.hitTest(mApplication, touch);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setHighlighted(final String id, final boolean highlighted) throws Exception {
|
||||||
|
final Object obj = mObjectTracker.get(id);
|
||||||
|
if (obj == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final NodeDescriptor<Object> descriptor = descriptorForObject(obj);
|
||||||
|
if (descriptor == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
descriptor.setHighlighted(obj, highlighted);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SearchResultNode searchTree(String query, Object obj) throws Exception {
|
||||||
|
final NodeDescriptor descriptor = descriptorForObject(obj);
|
||||||
|
List<SearchResultNode> childTrees = null;
|
||||||
|
boolean isMatch = descriptor.matches(query, obj);
|
||||||
|
|
||||||
|
for (int i = 0; i < descriptor.getChildCount(obj); i++) {
|
||||||
|
Object child = descriptor.getChildAt(obj, i);
|
||||||
|
SearchResultNode childNode = searchTree(query, child);
|
||||||
|
if (childNode != null) {
|
||||||
|
if (childTrees == null) {
|
||||||
|
childTrees = new ArrayList<>();
|
||||||
|
}
|
||||||
|
childTrees.add(childNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMatch || childTrees != null) {
|
||||||
|
final String id = trackObject(obj);
|
||||||
|
return new SearchResultNode(id, isMatch, getNode(id), childTrees);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private @Nullable SonarObject getNode(String id) throws Exception {
|
||||||
|
final Object obj = mObjectTracker.get(id);
|
||||||
|
if (obj == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final NodeDescriptor<Object> descriptor = descriptorForObject(obj);
|
||||||
|
if (descriptor == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final SonarArray.Builder children = new SonarArray.Builder();
|
||||||
|
new ErrorReportingRunnable(mConnection) {
|
||||||
|
@Override
|
||||||
|
protected void runOrThrow() throws Exception {
|
||||||
|
for (int i = 0, count = descriptor.getChildCount(obj); i < count; i++) {
|
||||||
|
final Object child = assertNotNull(descriptor.getChildAt(obj, i));
|
||||||
|
children.put(trackObject(child));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.run();
|
||||||
|
|
||||||
|
final SonarObject.Builder data = new SonarObject.Builder();
|
||||||
|
new ErrorReportingRunnable(mConnection) {
|
||||||
|
@Override
|
||||||
|
protected void runOrThrow() throws Exception {
|
||||||
|
for (Named<SonarObject> props : descriptor.getData(obj)) {
|
||||||
|
data.put(props.getName(), props.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.run();
|
||||||
|
|
||||||
|
final SonarArray.Builder attributes = new SonarArray.Builder();
|
||||||
|
new ErrorReportingRunnable(mConnection) {
|
||||||
|
@Override
|
||||||
|
protected void runOrThrow() throws Exception {
|
||||||
|
for (Named<String> attribute : descriptor.getAttributes(obj)) {
|
||||||
|
attributes.put(
|
||||||
|
new SonarObject.Builder()
|
||||||
|
.put("name", attribute.getName())
|
||||||
|
.put("value", attribute.getValue())
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.run();
|
||||||
|
|
||||||
|
return new SonarObject.Builder()
|
||||||
|
.put("id", descriptor.getId(obj))
|
||||||
|
.put("name", descriptor.getName(obj))
|
||||||
|
.put("data", data)
|
||||||
|
.put("children", children)
|
||||||
|
.put("attributes", attributes)
|
||||||
|
.put("decoration", descriptor.getDecoration(obj))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String trackObject(Object obj) throws Exception {
|
||||||
|
final NodeDescriptor<Object> descriptor = descriptorForObject(obj);
|
||||||
|
final String id = descriptor.getId(obj);
|
||||||
|
final Object curr = mObjectTracker.get(id);
|
||||||
|
if (obj != curr) {
|
||||||
|
mObjectTracker.put(id, obj);
|
||||||
|
descriptor.init(obj);
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
private NodeDescriptor<Object> descriptorForObject(Object obj) {
|
||||||
|
final Class c = assertNotNull(obj).getClass();
|
||||||
|
return (NodeDescriptor<Object>) mDescriptorMapping.descriptorForClass(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Object assertNotNull(@Nullable Object o) {
|
||||||
|
if (o == null) {
|
||||||
|
throw new AssertionError("Unexpected null value");
|
||||||
|
}
|
||||||
|
return o;
|
||||||
|
}
|
||||||
|
}
|
||||||
79
android/plugins/inspector/InspectorValue.java
Normal file
79
android/plugins/inspector/InspectorValue.java
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* 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.sonar.plugins.inspector;
|
||||||
|
|
||||||
|
import com.facebook.sonar.core.SonarObject;
|
||||||
|
import com.facebook.sonar.core.SonarValue;
|
||||||
|
|
||||||
|
public class InspectorValue<T> implements SonarValue {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Descrive the type of data this value contains. This will influence how values are parsed and
|
||||||
|
* displayed by the Sonar desktop app. For example colors will be parse as integers and displayed
|
||||||
|
* using hex values and be editable using a color picker.
|
||||||
|
*
|
||||||
|
* <p>Do not extends this list of types without adding support for the type in the desktop
|
||||||
|
* Inspector.
|
||||||
|
*/
|
||||||
|
public static class Type<T> {
|
||||||
|
|
||||||
|
public static final Type Auto = new Type<>("auto");
|
||||||
|
public static final Type<String> Text = new Type<>("text");
|
||||||
|
public static final Type<Number> Number = new Type<>("number");
|
||||||
|
public static final Type<Boolean> Boolean = new Type<>("boolean");
|
||||||
|
public static final Type<String> Enum = new Type<>("enum");
|
||||||
|
public static final Type<Integer> Color = new Type<>("color");
|
||||||
|
|
||||||
|
private final String mName;
|
||||||
|
|
||||||
|
Type(String name) {
|
||||||
|
mName = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return mName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final Type<T> mType;
|
||||||
|
final T mValue;
|
||||||
|
final boolean mMutable;
|
||||||
|
|
||||||
|
private InspectorValue(Type<T> type, T value, boolean mutable) {
|
||||||
|
mType = type;
|
||||||
|
mValue = value;
|
||||||
|
mMutable = mutable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> InspectorValue<T> mutable(Type<T> type, T value) {
|
||||||
|
return new InspectorValue<>(type, value, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> InspectorValue<T> immutable(Type<T> type, T value) {
|
||||||
|
return new InspectorValue<>(type, value, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static InspectorValue mutable(Object value) {
|
||||||
|
return new InspectorValue<>(Type.Auto, value, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static InspectorValue immutable(Object value) {
|
||||||
|
return new InspectorValue<>(Type.Auto, value, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SonarObject toSonarObject() {
|
||||||
|
return new SonarObject.Builder()
|
||||||
|
.put("__type__", mType)
|
||||||
|
.put("__mutable__", mMutable)
|
||||||
|
.put("value", mValue)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
27
android/plugins/inspector/Named.java
Normal file
27
android/plugins/inspector/Named.java
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* 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.sonar.plugins.inspector;
|
||||||
|
|
||||||
|
public class Named<ValueType> {
|
||||||
|
private final String mName;
|
||||||
|
private final ValueType mValue;
|
||||||
|
|
||||||
|
public Named(String name, ValueType value) {
|
||||||
|
mName = name;
|
||||||
|
mValue = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return mName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueType getValue() {
|
||||||
|
return mValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
143
android/plugins/inspector/NodeDescriptor.java
Normal file
143
android/plugins/inspector/NodeDescriptor.java
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* 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.sonar.plugins.inspector;
|
||||||
|
|
||||||
|
import com.facebook.sonar.core.SonarArray;
|
||||||
|
import com.facebook.sonar.core.SonarConnection;
|
||||||
|
import com.facebook.sonar.core.SonarDynamic;
|
||||||
|
import com.facebook.sonar.core.SonarObject;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A NodeDescriptor is an object which known how to expose an Object of type T to the ew Inspector.
|
||||||
|
* This class is the extension point for the Sonar inspector plugin and is how custom classes and
|
||||||
|
* data can be exposed to the inspector.
|
||||||
|
*/
|
||||||
|
public abstract class NodeDescriptor<T> {
|
||||||
|
private SonarConnection mConnection;
|
||||||
|
private DescriptorMapping mDescriptorMapping;
|
||||||
|
|
||||||
|
void setConnection(SonarConnection connection) {
|
||||||
|
mConnection = connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setDescriptorMapping(DescriptorMapping descriptorMapping) {
|
||||||
|
mDescriptorMapping = descriptorMapping;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The descriptor for a given class. This is useful for when a descriptor wants to
|
||||||
|
* delegate parts of its implementation to another descriptor, say the super class of the
|
||||||
|
* object it describes. This is highly encouraged instead of subclassing another descriptor
|
||||||
|
* class.
|
||||||
|
*/
|
||||||
|
protected final NodeDescriptor<?> descriptorForClass(Class<?> clazz) {
|
||||||
|
return mDescriptorMapping.descriptorForClass(clazz);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate a node. This tells Sonar that this node is no longer valid and its properties and/or
|
||||||
|
* children have changed. This will trigger Sonar to re-query this node getting any new data.
|
||||||
|
*/
|
||||||
|
protected final void invalidate(final T node) {
|
||||||
|
if (mConnection != null) {
|
||||||
|
new ErrorReportingRunnable() {
|
||||||
|
@Override
|
||||||
|
protected void runOrThrow() throws Exception {
|
||||||
|
SonarArray array =
|
||||||
|
new SonarArray.Builder()
|
||||||
|
.put(new SonarObject.Builder().put("id", getId(node)).build())
|
||||||
|
.build();
|
||||||
|
SonarObject params = new SonarObject.Builder().put("nodes", array).build();
|
||||||
|
mConnection.send("invalidate", params);
|
||||||
|
}
|
||||||
|
}.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected final boolean connected() {
|
||||||
|
return mConnection != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract class ErrorReportingRunnable
|
||||||
|
extends com.facebook.sonar.core.ErrorReportingRunnable {
|
||||||
|
public ErrorReportingRunnable() {
|
||||||
|
super(mConnection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize a node. This implementation usually consists of setting up listeners to know when to
|
||||||
|
* call {@link NodeDescriptor#invalidate(Object)}.
|
||||||
|
*/
|
||||||
|
public abstract void init(T node) throws Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A globally unique ID used to identify a node in a hierarchy. If your node does not have a
|
||||||
|
* globally unique ID it is fine to rely on {@link System#identityHashCode(Object)}.
|
||||||
|
*/
|
||||||
|
public abstract String getId(T node) throws Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name used to identify this node in the inspector. Does not need to be unique. A good
|
||||||
|
* default is to use the class name of the node.
|
||||||
|
*/
|
||||||
|
public abstract String getName(T node) throws Exception;
|
||||||
|
|
||||||
|
/** @return The number of children this node exposes in the inspector. */
|
||||||
|
public abstract int getChildCount(T node) throws Exception;
|
||||||
|
|
||||||
|
/** @return The child at index. */
|
||||||
|
public abstract Object getChildAt(T node, int index) throws Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the data to show for this node in the sidebar of the inspector. The object will be showen
|
||||||
|
* in order and with a header matching the given name.
|
||||||
|
*/
|
||||||
|
public abstract List<Named<SonarObject>> getData(T node) throws Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a value on the provided node at the given path. The path will match a key path in the data
|
||||||
|
* provided by {@link this#getData(Object)} and the value will be of the same type as the value
|
||||||
|
* mathcing that path in the returned object.
|
||||||
|
*/
|
||||||
|
public abstract void setValue(T node, String[] path, SonarDynamic value) throws Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the attributes for this node. This is a list of read-only string:string mapping which show
|
||||||
|
* up inline in the elements inspector. See {@link Named} for more information.
|
||||||
|
*/
|
||||||
|
public abstract List<Named<String>> getAttributes(T node) throws Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Highlight this node. Use {@link HighlightedOverlay} if possible. This is used to highlight a
|
||||||
|
* node which is selected in the inspector. The plugin automatically takes care of de-selecting
|
||||||
|
* the previously highlighted node.
|
||||||
|
*/
|
||||||
|
public abstract void setHighlighted(T node, boolean selected) throws Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform hit testing on the given node. Either continue the search in a child with {@link
|
||||||
|
* Touch#continueWithOffset(int, int, int)} or finish the hit testing on this node with {@link
|
||||||
|
* Touch#finish()}
|
||||||
|
*/
|
||||||
|
public abstract void hitTest(T node, Touch touch) throws Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return A string indicating how this element should be decorated. Check with the Sonar desktop
|
||||||
|
* app to see what values are supported.
|
||||||
|
*/
|
||||||
|
public abstract String getDecoration(T node) throws Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test this node against a given query to see if it matches. This is used for finding search
|
||||||
|
* results.
|
||||||
|
*/
|
||||||
|
public abstract boolean matches(String query, T node) throws Exception;
|
||||||
|
}
|
||||||
45
android/plugins/inspector/ObjectTracker.java
Normal file
45
android/plugins/inspector/ObjectTracker.java
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* 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.sonar.plugins.inspector;
|
||||||
|
|
||||||
|
import java.lang.ref.WeakReference;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
class ObjectTracker {
|
||||||
|
private final Map<String, WeakReference<Object>> mObjects = new HashMap<>();
|
||||||
|
|
||||||
|
void put(String id, Object obj) {
|
||||||
|
mObjects.put(id, new WeakReference<>(obj));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
Object get(String id) {
|
||||||
|
final WeakReference<Object> weakObj = mObjects.get(id);
|
||||||
|
if (weakObj == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Object obj = weakObj.get();
|
||||||
|
if (obj == null) {
|
||||||
|
mObjects.remove(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
void clear() {
|
||||||
|
mObjects.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean contains(String id) {
|
||||||
|
return mObjects.containsKey(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
51
android/plugins/inspector/SearchResultNode.java
Normal file
51
android/plugins/inspector/SearchResultNode.java
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* 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.sonar.plugins.inspector;
|
||||||
|
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import com.facebook.sonar.core.SonarArray;
|
||||||
|
import com.facebook.sonar.core.SonarObject;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class SearchResultNode {
|
||||||
|
|
||||||
|
private final String id;
|
||||||
|
private final boolean isMatch;
|
||||||
|
private final SonarObject element;
|
||||||
|
@Nullable
|
||||||
|
private final List<SearchResultNode> children;
|
||||||
|
|
||||||
|
SearchResultNode(
|
||||||
|
String id, boolean isMatch, SonarObject element, @Nullable List<SearchResultNode> children) {
|
||||||
|
this.id = id;
|
||||||
|
this.isMatch = isMatch;
|
||||||
|
this.element = element;
|
||||||
|
this.children = children;
|
||||||
|
}
|
||||||
|
|
||||||
|
SonarObject toSonarObject() {
|
||||||
|
final SonarArray childArray;
|
||||||
|
if (children != null) {
|
||||||
|
final SonarArray.Builder builder = new SonarArray.Builder();
|
||||||
|
for (SearchResultNode child : children) {
|
||||||
|
builder.put(child.toSonarObject());
|
||||||
|
}
|
||||||
|
childArray = builder.build();
|
||||||
|
} else {
|
||||||
|
childArray = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new SonarObject.Builder()
|
||||||
|
.put("id", this.id)
|
||||||
|
.put("isMatch", this.isMatch)
|
||||||
|
.put("element", this.element)
|
||||||
|
.put("children", childArray)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
14
android/plugins/inspector/SelfRegisteringNodeDescriptor.java
Normal file
14
android/plugins/inspector/SelfRegisteringNodeDescriptor.java
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* 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.sonar.plugins.inspector;
|
||||||
|
|
||||||
|
public abstract class SelfRegisteringNodeDescriptor<T> extends NodeDescriptor<T> {
|
||||||
|
|
||||||
|
public abstract void register(DescriptorMapping descriptorMapping);
|
||||||
|
}
|
||||||
31
android/plugins/inspector/Touch.java
Normal file
31
android/plugins/inspector/Touch.java
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* 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.sonar.plugins.inspector;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to collect the path from the root node to the node at point [x, y]. This is used for
|
||||||
|
* creating a node path to the targeting node from the root when performing hit testing.
|
||||||
|
*/
|
||||||
|
public interface Touch {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call this when the path has reached its destination. This should be called from the descriptor
|
||||||
|
* which described the leaf node containing [x, y].
|
||||||
|
*/
|
||||||
|
void finish();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Continue hit testing in child at the given index. Offseting the touch location to the child's
|
||||||
|
* coordinate system.
|
||||||
|
*/
|
||||||
|
void continueWithOffset(int childIndex, int offsetX, int offsetY);
|
||||||
|
|
||||||
|
/** @return Whether or not this Touch is contained within the provided bounds. */
|
||||||
|
boolean containedIn(int l, int t, int r, int b);
|
||||||
|
}
|
||||||
129
android/plugins/inspector/descriptors/ActivityDescriptor.java
Normal file
129
android/plugins/inspector/descriptors/ActivityDescriptor.java
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* 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.sonar.plugins.inspector.descriptors;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.view.Window;
|
||||||
|
import com.facebook.sonar.core.SonarDynamic;
|
||||||
|
import com.facebook.sonar.core.SonarObject;
|
||||||
|
import com.facebook.sonar.plugins.inspector.Named;
|
||||||
|
import com.facebook.sonar.plugins.inspector.NodeDescriptor;
|
||||||
|
import com.facebook.sonar.plugins.inspector.Touch;
|
||||||
|
import com.facebook.stetho.common.android.FragmentActivityAccessor;
|
||||||
|
import com.facebook.stetho.common.android.FragmentCompat;
|
||||||
|
import com.facebook.stetho.common.android.FragmentManagerAccessor;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
public class ActivityDescriptor extends NodeDescriptor<Activity> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(Activity node) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId(Activity node) {
|
||||||
|
return Integer.toString(System.identityHashCode(node));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName(Activity node) {
|
||||||
|
return node.getClass().getSimpleName();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getChildCount(Activity node) {
|
||||||
|
return (node.getWindow() != null ? 1 : 0)
|
||||||
|
+ getDialogFragments(FragmentCompat.getSupportLibInstance(), node).size()
|
||||||
|
+ getDialogFragments(FragmentCompat.getFrameworkInstance(), node).size();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getChildAt(Activity node, int index) {
|
||||||
|
if (node.getWindow() != null) {
|
||||||
|
if (index == 0) {
|
||||||
|
return node.getWindow();
|
||||||
|
} else {
|
||||||
|
index--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final List dialogs = getDialogFragments(FragmentCompat.getSupportLibInstance(), node);
|
||||||
|
if (index < dialogs.size()) {
|
||||||
|
return dialogs.get(index);
|
||||||
|
} else {
|
||||||
|
final List supportDialogs = getDialogFragments(FragmentCompat.getFrameworkInstance(), node);
|
||||||
|
return supportDialogs.get(index - dialogs.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Named<SonarObject>> getData(Activity node) {
|
||||||
|
return Collections.EMPTY_LIST;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setValue(Activity node, String[] path, SonarDynamic value) throws Exception {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Named<String>> getAttributes(Activity node) {
|
||||||
|
return Collections.EMPTY_LIST;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setHighlighted(Activity node, boolean selected) throws Exception {
|
||||||
|
final NodeDescriptor descriptor = descriptorForClass(Window.class);
|
||||||
|
descriptor.setHighlighted(node.getWindow(), selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void hitTest(Activity node, Touch touch) {
|
||||||
|
touch.continueWithOffset(0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable String getDecoration(Activity obj) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean matches(String query, Activity node) throws Exception {
|
||||||
|
NodeDescriptor descriptor = descriptorForClass(Object.class);
|
||||||
|
return descriptor.matches(query, node);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Object> getDialogFragments(FragmentCompat compat, Activity activity) {
|
||||||
|
if (compat == null || !compat.getFragmentActivityClass().isInstance(activity)) {
|
||||||
|
return Collections.EMPTY_LIST;
|
||||||
|
}
|
||||||
|
|
||||||
|
FragmentActivityAccessor activityAccessor = compat.forFragmentActivity();
|
||||||
|
Object fragmentManager = activityAccessor.getFragmentManager(activity);
|
||||||
|
if (fragmentManager == null) {
|
||||||
|
return Collections.EMPTY_LIST;
|
||||||
|
}
|
||||||
|
|
||||||
|
FragmentManagerAccessor fragmentManagerAccessor = compat.forFragmentManager();
|
||||||
|
List<Object> addedFragments = fragmentManagerAccessor.getAddedFragments(fragmentManager);
|
||||||
|
if (addedFragments == null) {
|
||||||
|
return Collections.EMPTY_LIST;
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<Object> dialogFragments = new ArrayList<>();
|
||||||
|
for (int i = 0, N = addedFragments.size(); i < N; ++i) {
|
||||||
|
final Object fragment = addedFragments.get(i);
|
||||||
|
if (compat.getDialogFragmentClass().isInstance(fragment)) {
|
||||||
|
dialogFragments.add(fragment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dialogFragments;
|
||||||
|
}
|
||||||
|
}
|
||||||
161
android/plugins/inspector/descriptors/ApplicationDescriptor.java
Normal file
161
android/plugins/inspector/descriptors/ApplicationDescriptor.java
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* 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.sonar.plugins.inspector.descriptors;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import com.facebook.sonar.core.SonarDynamic;
|
||||||
|
import com.facebook.sonar.core.SonarObject;
|
||||||
|
import com.facebook.sonar.plugins.inspector.ApplicationWrapper;
|
||||||
|
import com.facebook.sonar.plugins.inspector.Named;
|
||||||
|
import com.facebook.sonar.plugins.inspector.NodeDescriptor;
|
||||||
|
import com.facebook.sonar.plugins.inspector.Touch;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
public class ApplicationDescriptor extends NodeDescriptor<ApplicationWrapper> {
|
||||||
|
|
||||||
|
private class NodeKey {
|
||||||
|
private int[] mKey;
|
||||||
|
|
||||||
|
boolean set(ApplicationWrapper node) {
|
||||||
|
final List<View> roots = node.getViewRoots();
|
||||||
|
final int childCount = roots.size();
|
||||||
|
final int[] key = new int[childCount];
|
||||||
|
|
||||||
|
for (int i = 0; i < childCount; i++) {
|
||||||
|
final View child = roots.get(i);
|
||||||
|
key[i] = System.identityHashCode(child);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean changed = false;
|
||||||
|
if (mKey == null) {
|
||||||
|
changed = true;
|
||||||
|
} else if (mKey.length != key.length) {
|
||||||
|
changed = true;
|
||||||
|
} else {
|
||||||
|
for (int i = 0; i < childCount; i++) {
|
||||||
|
if (mKey[i] != key[i]) {
|
||||||
|
changed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mKey = key;
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(final ApplicationWrapper node) {
|
||||||
|
node.setListener(
|
||||||
|
new ApplicationWrapper.ActivityStackChangedListener() {
|
||||||
|
@Override
|
||||||
|
public void onActivityStackChanged(List<Activity> stack) {
|
||||||
|
invalidate(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
final NodeKey key = new NodeKey();
|
||||||
|
final Runnable maybeInvalidate =
|
||||||
|
new NodeDescriptor.ErrorReportingRunnable() {
|
||||||
|
@Override
|
||||||
|
public void runOrThrow() throws Exception {
|
||||||
|
if (connected()) {
|
||||||
|
if (key.set(node)) {
|
||||||
|
invalidate(node);
|
||||||
|
}
|
||||||
|
node.postDelayed(this, 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
node.postDelayed(maybeInvalidate, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId(ApplicationWrapper node) {
|
||||||
|
return node.getApplication().getPackageName();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName(ApplicationWrapper node) {
|
||||||
|
return node.getApplication().getPackageName();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getChildCount(ApplicationWrapper node) {
|
||||||
|
return node.getViewRoots().size();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getChildAt(ApplicationWrapper node, int index) {
|
||||||
|
final View view = node.getViewRoots().get(index);
|
||||||
|
|
||||||
|
for (Activity activity : node.getActivityStack()) {
|
||||||
|
if (activity.getWindow().getDecorView() == view) {
|
||||||
|
return activity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Named<SonarObject>> getData(ApplicationWrapper node) {
|
||||||
|
return Collections.EMPTY_LIST;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setValue(ApplicationWrapper node, String[] path, SonarDynamic value) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Named<String>> getAttributes(ApplicationWrapper node) {
|
||||||
|
return Collections.EMPTY_LIST;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setHighlighted(ApplicationWrapper node, boolean selected) throws Exception {
|
||||||
|
final int childCount = getChildCount(node);
|
||||||
|
if (childCount > 0) {
|
||||||
|
final Object topChild = getChildAt(node, childCount - 1);
|
||||||
|
final NodeDescriptor descriptor = descriptorForClass(topChild.getClass());
|
||||||
|
descriptor.setHighlighted(topChild, selected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void hitTest(ApplicationWrapper node, Touch touch) {
|
||||||
|
final int childCount = getChildCount(node);
|
||||||
|
|
||||||
|
for (int i = childCount - 1; i >= 0; i--) {
|
||||||
|
final Object child = getChildAt(node, i);
|
||||||
|
if (child instanceof Activity || child instanceof ViewGroup) {
|
||||||
|
touch.continueWithOffset(i, 0, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
touch.finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable String getDecoration(ApplicationWrapper obj) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean matches(String query, ApplicationWrapper node) throws Exception {
|
||||||
|
NodeDescriptor descriptor = descriptorForClass(Object.class);
|
||||||
|
return descriptor.matches(query, node);
|
||||||
|
}
|
||||||
|
}
|
||||||
81
android/plugins/inspector/descriptors/DialogDescriptor.java
Normal file
81
android/plugins/inspector/descriptors/DialogDescriptor.java
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* 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.sonar.plugins.inspector.descriptors;
|
||||||
|
|
||||||
|
import android.app.Dialog;
|
||||||
|
import android.view.Window;
|
||||||
|
import com.facebook.sonar.core.SonarDynamic;
|
||||||
|
import com.facebook.sonar.core.SonarObject;
|
||||||
|
import com.facebook.sonar.plugins.inspector.Named;
|
||||||
|
import com.facebook.sonar.plugins.inspector.NodeDescriptor;
|
||||||
|
import com.facebook.sonar.plugins.inspector.Touch;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
public class DialogDescriptor extends NodeDescriptor<Dialog> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(Dialog node) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId(Dialog node) {
|
||||||
|
return Integer.toString(System.identityHashCode(node));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName(Dialog node) {
|
||||||
|
return node.getClass().getSimpleName();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getChildCount(Dialog node) {
|
||||||
|
return node.getWindow() == null ? 0 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getChildAt(Dialog node, int index) {
|
||||||
|
return node.getWindow();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Named<SonarObject>> getData(Dialog node) {
|
||||||
|
return Collections.EMPTY_LIST;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setValue(Dialog node, String[] path, SonarDynamic value) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Named<String>> getAttributes(Dialog node) {
|
||||||
|
return Collections.EMPTY_LIST;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setHighlighted(Dialog node, boolean selected) throws Exception {
|
||||||
|
final NodeDescriptor descriptor = descriptorForClass(Window.class);
|
||||||
|
descriptor.setHighlighted(node.getWindow(), selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void hitTest(Dialog node, Touch touch) {
|
||||||
|
touch.continueWithOffset(0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable String getDecoration(Dialog obj) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean matches(String query, Dialog node) throws Exception {
|
||||||
|
NodeDescriptor descriptor = descriptorForClass(Object.class);
|
||||||
|
return descriptor.matches(query, node);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* 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.sonar.plugins.inspector.descriptors;
|
||||||
|
|
||||||
|
import android.app.Dialog;
|
||||||
|
import android.app.DialogFragment;
|
||||||
|
import android.app.Fragment;
|
||||||
|
import com.facebook.sonar.core.SonarDynamic;
|
||||||
|
import com.facebook.sonar.core.SonarObject;
|
||||||
|
import com.facebook.sonar.plugins.inspector.Named;
|
||||||
|
import com.facebook.sonar.plugins.inspector.NodeDescriptor;
|
||||||
|
import com.facebook.sonar.plugins.inspector.Touch;
|
||||||
|
import java.util.List;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
public class DialogFragmentDescriptor extends NodeDescriptor<DialogFragment> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(DialogFragment node) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId(DialogFragment node) throws Exception {
|
||||||
|
final NodeDescriptor descriptor = descriptorForClass(Fragment.class);
|
||||||
|
return descriptor.getId(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName(DialogFragment node) throws Exception {
|
||||||
|
final NodeDescriptor descriptor = descriptorForClass(Fragment.class);
|
||||||
|
return descriptor.getName(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getChildCount(DialogFragment node) {
|
||||||
|
return node.getDialog() == null ? 0 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getChildAt(DialogFragment node, int index) {
|
||||||
|
return node.getDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Named<SonarObject>> getData(DialogFragment node) throws Exception {
|
||||||
|
final NodeDescriptor descriptor = descriptorForClass(Fragment.class);
|
||||||
|
return descriptor.getData(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setValue(DialogFragment node, String[] path, SonarDynamic value) throws Exception {
|
||||||
|
final NodeDescriptor descriptor = descriptorForClass(Fragment.class);
|
||||||
|
descriptor.setValue(node, path, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Named<String>> getAttributes(DialogFragment node) throws Exception {
|
||||||
|
final NodeDescriptor descriptor = descriptorForClass(Fragment.class);
|
||||||
|
return descriptor.getAttributes(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setHighlighted(DialogFragment node, boolean selected) throws Exception {
|
||||||
|
final NodeDescriptor descriptor = descriptorForClass(Dialog.class);
|
||||||
|
if (node.getDialog() != null) {
|
||||||
|
descriptor.setHighlighted(node.getDialog(), selected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void hitTest(DialogFragment node, Touch touch) {
|
||||||
|
touch.continueWithOffset(0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable String getDecoration(DialogFragment obj) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean matches(String query, DialogFragment node) throws Exception {
|
||||||
|
NodeDescriptor descriptor = descriptorForClass(Object.class);
|
||||||
|
return descriptor.matches(query, node);
|
||||||
|
}
|
||||||
|
}
|
||||||
146
android/plugins/inspector/descriptors/DrawableDescriptor.java
Normal file
146
android/plugins/inspector/descriptors/DrawableDescriptor.java
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* 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.sonar.plugins.inspector.descriptors;
|
||||||
|
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.view.View;
|
||||||
|
import com.facebook.sonar.core.SonarDynamic;
|
||||||
|
import com.facebook.sonar.core.SonarObject;
|
||||||
|
import com.facebook.sonar.plugins.inspector.HighlightedOverlay;
|
||||||
|
import com.facebook.sonar.plugins.inspector.InspectorValue;
|
||||||
|
import com.facebook.sonar.plugins.inspector.Named;
|
||||||
|
import com.facebook.sonar.plugins.inspector.NodeDescriptor;
|
||||||
|
import com.facebook.sonar.plugins.inspector.Touch;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
public class DrawableDescriptor extends NodeDescriptor<Drawable> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(Drawable node) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId(Drawable node) {
|
||||||
|
return Integer.toString(System.identityHashCode(node));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName(Drawable node) {
|
||||||
|
return node.getClass().getSimpleName();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getChildCount(Drawable node) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable Object getChildAt(Drawable node, int index) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Named<SonarObject>> getData(Drawable node) {
|
||||||
|
final SonarObject.Builder props = new SonarObject.Builder();
|
||||||
|
final Rect bounds = node.getBounds();
|
||||||
|
|
||||||
|
props.put("left", InspectorValue.mutable(bounds.left));
|
||||||
|
props.put("top", InspectorValue.mutable(bounds.top));
|
||||||
|
props.put("right", InspectorValue.mutable(bounds.right));
|
||||||
|
props.put("bottom", InspectorValue.mutable(bounds.bottom));
|
||||||
|
|
||||||
|
if (hasAlphaSupport()) {
|
||||||
|
props.put("alpha", InspectorValue.mutable(node.getAlpha()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Arrays.asList(new Named<>("Drawable", props.build()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setValue(Drawable node, String[] path, SonarDynamic value) {
|
||||||
|
final Rect bounds = node.getBounds();
|
||||||
|
|
||||||
|
switch (path[0]) {
|
||||||
|
case "Drawable":
|
||||||
|
switch (path[1]) {
|
||||||
|
case "left":
|
||||||
|
bounds.left = value.asInt();
|
||||||
|
node.setBounds(bounds);
|
||||||
|
break;
|
||||||
|
case "top":
|
||||||
|
bounds.top = value.asInt();
|
||||||
|
node.setBounds(bounds);
|
||||||
|
break;
|
||||||
|
case "right":
|
||||||
|
bounds.right = value.asInt();
|
||||||
|
node.setBounds(bounds);
|
||||||
|
break;
|
||||||
|
case "bottom":
|
||||||
|
bounds.bottom = value.asInt();
|
||||||
|
node.setBounds(bounds);
|
||||||
|
break;
|
||||||
|
case "alpha":
|
||||||
|
node.setAlpha(value.asInt());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean hasAlphaSupport() {
|
||||||
|
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Named<String>> getAttributes(Drawable node) {
|
||||||
|
return Collections.EMPTY_LIST;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setHighlighted(Drawable node, boolean selected) {
|
||||||
|
// Ensure we handle wrapping drawable
|
||||||
|
Drawable.Callback callbacks = node.getCallback();
|
||||||
|
while (callbacks instanceof Drawable) {
|
||||||
|
callbacks = ((Drawable) callbacks).getCallback();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(callbacks instanceof View)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final View callbackView = (View) callbacks;
|
||||||
|
if (selected) {
|
||||||
|
final Rect zero = new Rect();
|
||||||
|
final Rect bounds = node.getBounds();
|
||||||
|
HighlightedOverlay.setHighlighted(callbackView, zero, zero, bounds);
|
||||||
|
} else {
|
||||||
|
HighlightedOverlay.removeHighlight(callbackView);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void hitTest(Drawable node, Touch touch) {
|
||||||
|
touch.finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable String getDecoration(Drawable obj) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean matches(String query, Drawable node) throws Exception {
|
||||||
|
NodeDescriptor descriptor = descriptorForClass(Object.class);
|
||||||
|
return descriptor.matches(query, node);
|
||||||
|
}
|
||||||
|
}
|
||||||
124
android/plugins/inspector/descriptors/FragmentDescriptor.java
Normal file
124
android/plugins/inspector/descriptors/FragmentDescriptor.java
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* 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.sonar.plugins.inspector.descriptors;
|
||||||
|
|
||||||
|
import android.app.Fragment;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.View;
|
||||||
|
import com.facebook.sonar.core.SonarDynamic;
|
||||||
|
import com.facebook.sonar.core.SonarObject;
|
||||||
|
import com.facebook.sonar.plugins.inspector.Named;
|
||||||
|
import com.facebook.sonar.plugins.inspector.NodeDescriptor;
|
||||||
|
import com.facebook.sonar.plugins.inspector.Touch;
|
||||||
|
import com.facebook.stetho.common.android.ResourcesUtil;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
public class FragmentDescriptor extends NodeDescriptor<Fragment> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(Fragment node) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId(Fragment node) {
|
||||||
|
return Integer.toString(System.identityHashCode(node));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName(Fragment node) {
|
||||||
|
return node.getClass().getSimpleName();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getChildCount(Fragment node) {
|
||||||
|
return node.getView() == null ? 0 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getChildAt(Fragment node, int index) {
|
||||||
|
return node.getView();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Named<SonarObject>> getData(Fragment node) {
|
||||||
|
final Bundle args = node.getArguments();
|
||||||
|
if (args == null || args.isEmpty()) {
|
||||||
|
return Collections.EMPTY_LIST;
|
||||||
|
}
|
||||||
|
|
||||||
|
final SonarObject.Builder bundle = new SonarObject.Builder();
|
||||||
|
|
||||||
|
for (String key : args.keySet()) {
|
||||||
|
bundle.put(key, args.get(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Arrays.asList(new Named<>("Arguments", bundle.build()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setValue(Fragment node, String[] path, SonarDynamic value) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Named<String>> getAttributes(Fragment node) {
|
||||||
|
final String resourceId = getResourceId(node);
|
||||||
|
|
||||||
|
if (resourceId == null) {
|
||||||
|
return Collections.EMPTY_LIST;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Arrays.asList(new Named<>("id", resourceId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private static String getResourceId(Fragment node) {
|
||||||
|
final int id = node.getId();
|
||||||
|
|
||||||
|
if (id == View.NO_ID || node.getHost() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResourcesUtil.getIdStringQuietly(node.getContext(), node.getResources(), id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setHighlighted(Fragment node, boolean selected) throws Exception {
|
||||||
|
if (node.getView() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final NodeDescriptor descriptor = descriptorForClass(View.class);
|
||||||
|
descriptor.setHighlighted(node.getView(), selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void hitTest(Fragment node, Touch touch) {
|
||||||
|
touch.continueWithOffset(0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable String getDecoration(Fragment obj) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean matches(String query, Fragment node) throws Exception {
|
||||||
|
final String resourceId = getResourceId(node);
|
||||||
|
|
||||||
|
if (resourceId != null) {
|
||||||
|
if (resourceId.toLowerCase().contains(query)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final NodeDescriptor objectDescriptor = descriptorForClass(Object.class);
|
||||||
|
return objectDescriptor.matches(query, node);
|
||||||
|
}
|
||||||
|
}
|
||||||
85
android/plugins/inspector/descriptors/ObjectDescriptor.java
Normal file
85
android/plugins/inspector/descriptors/ObjectDescriptor.java
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* 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.sonar.plugins.inspector.descriptors;
|
||||||
|
|
||||||
|
import com.facebook.sonar.core.SonarDynamic;
|
||||||
|
import com.facebook.sonar.core.SonarObject;
|
||||||
|
import com.facebook.sonar.plugins.inspector.Named;
|
||||||
|
import com.facebook.sonar.plugins.inspector.NodeDescriptor;
|
||||||
|
import com.facebook.sonar.plugins.inspector.Touch;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
public class ObjectDescriptor extends NodeDescriptor<Object> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(Object node) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId(Object node) {
|
||||||
|
return Integer.toString(System.identityHashCode(node));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName(Object node) {
|
||||||
|
return node.getClass().getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getChildCount(Object node) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable Object getChildAt(Object node, int index) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Named<SonarObject>> getData(Object node) {
|
||||||
|
return Collections.EMPTY_LIST;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setValue(Object node, String[] path, SonarDynamic value) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Named<String>> getAttributes(Object node) {
|
||||||
|
return Collections.EMPTY_LIST;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setHighlighted(Object node, boolean selected) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void hitTest(Object node, Touch touch) {
|
||||||
|
touch.finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable String getDecoration(Object obj) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean matches(String query, Object node) throws Exception {
|
||||||
|
final NodeDescriptor descriptor = descriptorForClass(node.getClass());
|
||||||
|
final List<Named<String>> attributes = descriptor.getAttributes(node);
|
||||||
|
for (Named<String> namedString : attributes) {
|
||||||
|
if (namedString.getName().equals("id")) {
|
||||||
|
if (namedString.getValue().toLowerCase().contains(query)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return descriptor.getName(node).toLowerCase().contains(query);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* 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.sonar.plugins.inspector.descriptors;
|
||||||
|
|
||||||
|
import android.app.Dialog;
|
||||||
|
import android.support.v4.app.DialogFragment;
|
||||||
|
import android.support.v4.app.Fragment;
|
||||||
|
import com.facebook.sonar.core.SonarDynamic;
|
||||||
|
import com.facebook.sonar.core.SonarObject;
|
||||||
|
import com.facebook.sonar.plugins.inspector.Named;
|
||||||
|
import com.facebook.sonar.plugins.inspector.NodeDescriptor;
|
||||||
|
import com.facebook.sonar.plugins.inspector.Touch;
|
||||||
|
import java.util.List;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
public class SupportDialogFragmentDescriptor extends NodeDescriptor<DialogFragment> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(DialogFragment node) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId(DialogFragment node) throws Exception {
|
||||||
|
final NodeDescriptor descriptor = descriptorForClass(Fragment.class);
|
||||||
|
return descriptor.getId(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName(DialogFragment node) throws Exception {
|
||||||
|
final NodeDescriptor descriptor = descriptorForClass(Fragment.class);
|
||||||
|
return descriptor.getName(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getChildCount(DialogFragment node) {
|
||||||
|
return node.getDialog() == null ? 0 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getChildAt(DialogFragment node, int index) {
|
||||||
|
return node.getDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Named<SonarObject>> getData(DialogFragment node) throws Exception {
|
||||||
|
final NodeDescriptor descriptor = descriptorForClass(Fragment.class);
|
||||||
|
return descriptor.getData(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setValue(DialogFragment node, String[] path, SonarDynamic value) throws Exception {
|
||||||
|
final NodeDescriptor descriptor = descriptorForClass(Fragment.class);
|
||||||
|
descriptor.setValue(node, path, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Named<String>> getAttributes(DialogFragment node) throws Exception {
|
||||||
|
final NodeDescriptor descriptor = descriptorForClass(Fragment.class);
|
||||||
|
return descriptor.getAttributes(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setHighlighted(DialogFragment node, boolean selected) throws Exception {
|
||||||
|
final NodeDescriptor descriptor = descriptorForClass(Dialog.class);
|
||||||
|
if (node.getDialog() != null) {
|
||||||
|
descriptor.setHighlighted(node.getDialog(), selected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void hitTest(DialogFragment node, Touch touch) {
|
||||||
|
touch.continueWithOffset(0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable String getDecoration(DialogFragment obj) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean matches(String query, DialogFragment node) throws Exception {
|
||||||
|
final NodeDescriptor descriptor = descriptorForClass(Object.class);
|
||||||
|
return descriptor.matches(query, node);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* 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.sonar.plugins.inspector.descriptors;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.support.v4.app.Fragment;
|
||||||
|
import android.view.View;
|
||||||
|
import com.facebook.sonar.core.SonarDynamic;
|
||||||
|
import com.facebook.sonar.core.SonarObject;
|
||||||
|
import com.facebook.sonar.plugins.inspector.Named;
|
||||||
|
import com.facebook.sonar.plugins.inspector.NodeDescriptor;
|
||||||
|
import com.facebook.sonar.plugins.inspector.Touch;
|
||||||
|
import com.facebook.stetho.common.android.ResourcesUtil;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
public class SupportFragmentDescriptor extends NodeDescriptor<Fragment> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(Fragment node) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId(Fragment node) {
|
||||||
|
return Integer.toString(System.identityHashCode(node));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName(Fragment node) {
|
||||||
|
return node.getClass().getSimpleName();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getChildCount(Fragment node) {
|
||||||
|
return node.getView() == null ? 0 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable Object getChildAt(Fragment node, int index) {
|
||||||
|
return node.getView();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Named<SonarObject>> getData(Fragment node) {
|
||||||
|
final Bundle args = node.getArguments();
|
||||||
|
if (args == null || args.isEmpty()) {
|
||||||
|
return Collections.EMPTY_LIST;
|
||||||
|
}
|
||||||
|
|
||||||
|
final SonarObject.Builder bundle = new SonarObject.Builder();
|
||||||
|
|
||||||
|
for (String key : args.keySet()) {
|
||||||
|
bundle.put(key, args.get(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Arrays.asList(new Named<>("Arguments", bundle.build()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setValue(Fragment node, String[] path, SonarDynamic value) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Named<String>> getAttributes(Fragment node) {
|
||||||
|
final int id = node.getId();
|
||||||
|
if (id == View.NO_ID || node.getHost() == null) {
|
||||||
|
return Collections.EMPTY_LIST;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Arrays.asList(
|
||||||
|
new Named<>(
|
||||||
|
"id", ResourcesUtil.getIdStringQuietly(node.getContext(), node.getResources(), id)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setHighlighted(Fragment node, boolean selected) throws Exception {
|
||||||
|
if (node.getView() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final NodeDescriptor descriptor = descriptorForClass(View.class);
|
||||||
|
descriptor.setHighlighted(node.getView(), selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void hitTest(Fragment node, Touch touch) {
|
||||||
|
touch.continueWithOffset(0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable String getDecoration(Fragment obj) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean matches(String query, Fragment node) throws Exception {
|
||||||
|
final NodeDescriptor descriptor = descriptorForClass(Object.class);
|
||||||
|
return descriptor.matches(query, node);
|
||||||
|
}
|
||||||
|
}
|
||||||
132
android/plugins/inspector/descriptors/TextViewDescriptor.java
Normal file
132
android/plugins/inspector/descriptors/TextViewDescriptor.java
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* 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.sonar.plugins.inspector.descriptors;
|
||||||
|
|
||||||
|
import static com.facebook.sonar.plugins.inspector.InspectorValue.Type.Color;
|
||||||
|
import static com.facebook.sonar.plugins.inspector.InspectorValue.Type.Number;
|
||||||
|
import static com.facebook.sonar.plugins.inspector.InspectorValue.Type.Text;
|
||||||
|
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import com.facebook.sonar.core.SonarDynamic;
|
||||||
|
import com.facebook.sonar.core.SonarObject;
|
||||||
|
import com.facebook.sonar.plugins.inspector.InspectorValue;
|
||||||
|
import com.facebook.sonar.plugins.inspector.Named;
|
||||||
|
import com.facebook.sonar.plugins.inspector.NodeDescriptor;
|
||||||
|
import com.facebook.sonar.plugins.inspector.Touch;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
public class TextViewDescriptor extends NodeDescriptor<TextView> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(TextView node) throws Exception {
|
||||||
|
final NodeDescriptor descriptor = descriptorForClass(View.class);
|
||||||
|
descriptor.init(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId(TextView node) throws Exception {
|
||||||
|
final NodeDescriptor descriptor = descriptorForClass(View.class);
|
||||||
|
return descriptor.getId(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName(TextView node) throws Exception {
|
||||||
|
final NodeDescriptor descriptor = descriptorForClass(View.class);
|
||||||
|
return descriptor.getName(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getChildCount(TextView node) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable Object getChildAt(TextView node, int index) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Named<SonarObject>> getData(TextView node) throws Exception {
|
||||||
|
final List<Named<SonarObject>> props = new ArrayList<>();
|
||||||
|
final NodeDescriptor descriptor = descriptorForClass(View.class);
|
||||||
|
|
||||||
|
props.add(
|
||||||
|
0,
|
||||||
|
new Named<>(
|
||||||
|
"TextView",
|
||||||
|
new SonarObject.Builder()
|
||||||
|
.put("text", InspectorValue.mutable(Text, node.getText().toString()))
|
||||||
|
.put(
|
||||||
|
"textColor",
|
||||||
|
InspectorValue.mutable(Color, node.getTextColors().getDefaultColor()))
|
||||||
|
.put("textSize", InspectorValue.mutable(Number, node.getTextSize()))
|
||||||
|
.build()));
|
||||||
|
|
||||||
|
props.addAll(descriptor.getData(node));
|
||||||
|
|
||||||
|
return props;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setValue(TextView node, String[] path, SonarDynamic value) throws Exception {
|
||||||
|
switch (path[0]) {
|
||||||
|
case "TextView":
|
||||||
|
switch (path[1]) {
|
||||||
|
case "text":
|
||||||
|
node.setText(value.asString());
|
||||||
|
break;
|
||||||
|
case "textColor":
|
||||||
|
node.setTextColor(value.asInt());
|
||||||
|
break;
|
||||||
|
case "textSize":
|
||||||
|
node.setTextSize(value.asInt());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
final NodeDescriptor descriptor = descriptorForClass(View.class);
|
||||||
|
descriptor.setValue(node, path, value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
invalidate(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Named<String>> getAttributes(TextView node) throws Exception {
|
||||||
|
final NodeDescriptor descriptor = descriptorForClass(View.class);
|
||||||
|
return descriptor.getAttributes(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setHighlighted(TextView node, boolean selected) throws Exception {
|
||||||
|
final NodeDescriptor descriptor = descriptorForClass(View.class);
|
||||||
|
descriptor.setHighlighted(node, selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void hitTest(TextView node, Touch touch) throws Exception {
|
||||||
|
final NodeDescriptor descriptor = descriptorForClass(View.class);
|
||||||
|
descriptor.hitTest(node, touch);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable String getDecoration(TextView node) throws Exception {
|
||||||
|
final NodeDescriptor descriptor = descriptorForClass(View.class);
|
||||||
|
return descriptor.getDecoration(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean matches(String query, TextView node) throws Exception {
|
||||||
|
final NodeDescriptor descriptor = descriptorForClass(Object.class);
|
||||||
|
return descriptor.matches(query, node);
|
||||||
|
}
|
||||||
|
}
|
||||||
708
android/plugins/inspector/descriptors/ViewDescriptor.java
Normal file
708
android/plugins/inspector/descriptors/ViewDescriptor.java
Normal file
@@ -0,0 +1,708 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* 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.sonar.plugins.inspector.descriptors;
|
||||||
|
|
||||||
|
import static com.facebook.sonar.plugins.inspector.InspectorValue.Type.Color;
|
||||||
|
import static com.facebook.sonar.plugins.inspector.InspectorValue.Type.Enum;
|
||||||
|
|
||||||
|
import android.annotation.TargetApi;
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import android.graphics.drawable.ColorDrawable;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.support.v4.view.MarginLayoutParamsCompat;
|
||||||
|
import android.support.v4.view.ViewCompat;
|
||||||
|
import android.util.SparseArray;
|
||||||
|
import android.view.Gravity;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.View.OnClickListener;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.ViewGroup.LayoutParams;
|
||||||
|
import android.view.ViewGroup.MarginLayoutParams;
|
||||||
|
import android.view.ViewParent;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import com.facebook.sonar.core.SonarDynamic;
|
||||||
|
import com.facebook.sonar.core.SonarObject;
|
||||||
|
import com.facebook.sonar.plugins.inspector.HighlightedOverlay;
|
||||||
|
import com.facebook.sonar.plugins.inspector.InspectorValue;
|
||||||
|
import com.facebook.sonar.plugins.inspector.Named;
|
||||||
|
import com.facebook.sonar.plugins.inspector.NodeDescriptor;
|
||||||
|
import com.facebook.sonar.plugins.inspector.Touch;
|
||||||
|
import com.facebook.sonar.plugins.inspector.descriptors.utils.AccessibilityUtil;
|
||||||
|
import com.facebook.sonar.plugins.inspector.descriptors.utils.EnumMapping;
|
||||||
|
import com.facebook.stetho.common.android.ResourcesUtil;
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
public class ViewDescriptor extends NodeDescriptor<View> {
|
||||||
|
|
||||||
|
private static Field sKeyedTagsField;
|
||||||
|
private static Field sListenerInfoField;
|
||||||
|
private static Field sOnClickListenerField;
|
||||||
|
|
||||||
|
static {
|
||||||
|
try {
|
||||||
|
sKeyedTagsField = View.class.getDeclaredField("mKeyedTags");
|
||||||
|
sKeyedTagsField.setAccessible(true);
|
||||||
|
|
||||||
|
sListenerInfoField = View.class.getDeclaredField("mListenerInfo");
|
||||||
|
sListenerInfoField.setAccessible(true);
|
||||||
|
|
||||||
|
final String viewInfoClassName = View.class.getName() + "$ListenerInfo";
|
||||||
|
sOnClickListenerField = Class.forName(viewInfoClassName).getDeclaredField("mOnClickListener");
|
||||||
|
sOnClickListenerField.setAccessible(true);
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(final View node) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId(View node) {
|
||||||
|
return Integer.toString(System.identityHashCode(node));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName(View node) {
|
||||||
|
return node.getClass().getSimpleName();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getChildCount(View node) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable Object getChildAt(View node, int index) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Named<SonarObject>> getData(View node) {
|
||||||
|
final SonarObject.Builder viewProps =
|
||||||
|
new SonarObject.Builder()
|
||||||
|
.put("height", InspectorValue.mutable(node.getHeight()))
|
||||||
|
.put("width", InspectorValue.mutable(node.getWidth()))
|
||||||
|
.put("alpha", InspectorValue.mutable(node.getAlpha()))
|
||||||
|
.put("visibility", sVisibilityMapping.get(node.getVisibility()))
|
||||||
|
.put("background", fromDrawable(node.getBackground()))
|
||||||
|
.put("tag", InspectorValue.mutable(node.getTag()))
|
||||||
|
.put("keyedTags", getTags(node))
|
||||||
|
.put("layoutParams", getLayoutParams(node))
|
||||||
|
.put(
|
||||||
|
"state",
|
||||||
|
new SonarObject.Builder()
|
||||||
|
.put("enabled", InspectorValue.mutable(node.isEnabled()))
|
||||||
|
.put("activated", InspectorValue.mutable(node.isActivated()))
|
||||||
|
.put("focused", node.isFocused())
|
||||||
|
.put("selected", InspectorValue.mutable(node.isSelected())))
|
||||||
|
.put(
|
||||||
|
"bounds",
|
||||||
|
new SonarObject.Builder()
|
||||||
|
.put("left", InspectorValue.mutable(node.getLeft()))
|
||||||
|
.put("right", InspectorValue.mutable(node.getRight()))
|
||||||
|
.put("top", InspectorValue.mutable(node.getTop()))
|
||||||
|
.put("bottom", InspectorValue.mutable(node.getBottom())))
|
||||||
|
.put(
|
||||||
|
"padding",
|
||||||
|
new SonarObject.Builder()
|
||||||
|
.put("left", InspectorValue.mutable(node.getPaddingLeft()))
|
||||||
|
.put("top", InspectorValue.mutable(node.getPaddingTop()))
|
||||||
|
.put("right", InspectorValue.mutable(node.getPaddingRight()))
|
||||||
|
.put("bottom", InspectorValue.mutable(node.getPaddingBottom())))
|
||||||
|
.put(
|
||||||
|
"rotation",
|
||||||
|
new SonarObject.Builder()
|
||||||
|
.put("x", InspectorValue.mutable(node.getRotationX()))
|
||||||
|
.put("y", InspectorValue.mutable(node.getRotationY()))
|
||||||
|
.put("z", InspectorValue.mutable(node.getRotation())))
|
||||||
|
.put(
|
||||||
|
"scale",
|
||||||
|
new SonarObject.Builder()
|
||||||
|
.put("x", InspectorValue.mutable(node.getScaleX()))
|
||||||
|
.put("y", InspectorValue.mutable(node.getScaleY())))
|
||||||
|
.put(
|
||||||
|
"pivot",
|
||||||
|
new SonarObject.Builder()
|
||||||
|
.put("x", InspectorValue.mutable(node.getPivotX()))
|
||||||
|
.put("y", InspectorValue.mutable(node.getPivotY())));
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||||
|
viewProps
|
||||||
|
.put("layoutDirection", sLayoutDirectionMapping.get(node.getLayoutDirection()))
|
||||||
|
.put("textDirection", sTextDirectionMapping.get(node.getTextDirection()))
|
||||||
|
.put("textAlignment", sTextAlignmentMapping.get(node.getTextAlignment()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
viewProps.put("elevation", InspectorValue.mutable(node.getElevation()));
|
||||||
|
}
|
||||||
|
|
||||||
|
SonarObject.Builder translation =
|
||||||
|
new SonarObject.Builder()
|
||||||
|
.put("x", InspectorValue.mutable(node.getTranslationX()))
|
||||||
|
.put("y", InspectorValue.mutable(node.getTranslationY()));
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
translation.put("z", InspectorValue.mutable(node.getTranslationZ()));
|
||||||
|
}
|
||||||
|
viewProps.put("translation", translation);
|
||||||
|
|
||||||
|
SonarObject.Builder position =
|
||||||
|
new SonarObject.Builder()
|
||||||
|
.put("x", InspectorValue.mutable(node.getX()))
|
||||||
|
.put("y", InspectorValue.mutable(node.getY()));
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
|
||||||
|
position.put("z", InspectorValue.mutable(node.getZ()));
|
||||||
|
}
|
||||||
|
viewProps.put("position", position);
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
viewProps.put("foreground", fromDrawable(node.getForeground()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Arrays.asList(
|
||||||
|
new Named<>("View", viewProps.build()),
|
||||||
|
new Named<>("Accessibility", getAccessibilityData(node)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SonarObject getAccessibilityData(View view) {
|
||||||
|
final SonarObject.Builder accessibilityProps = new SonarObject.Builder();
|
||||||
|
|
||||||
|
// This needs to be an empty string to be mutable. See t20470623.
|
||||||
|
CharSequence contentDescription =
|
||||||
|
view.getContentDescription() != null ? view.getContentDescription() : "";
|
||||||
|
accessibilityProps.put("content-description", InspectorValue.mutable(contentDescription));
|
||||||
|
accessibilityProps.put("focusable", InspectorValue.mutable(view.isFocusable()));
|
||||||
|
accessibilityProps.put("node-info", AccessibilityUtil.getAccessibilityNodeInfoProperties(view));
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||||
|
accessibilityProps.put(
|
||||||
|
"important-for-accessibility",
|
||||||
|
AccessibilityUtil.sImportantForAccessibilityMapping.get(
|
||||||
|
view.getImportantForAccessibility()));
|
||||||
|
}
|
||||||
|
|
||||||
|
AccessibilityUtil.addTalkbackProperties(accessibilityProps, view);
|
||||||
|
|
||||||
|
return accessibilityProps.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
|
||||||
|
@Override
|
||||||
|
public void setValue(View node, String[] path, SonarDynamic value) {
|
||||||
|
if (path[0].equals("Accessibility")) {
|
||||||
|
setAccessibilityValue(node, path, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!path[0].equals("View")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (path[1]) {
|
||||||
|
case "elevation":
|
||||||
|
node.setElevation(value.asFloat());
|
||||||
|
break;
|
||||||
|
case "alpha":
|
||||||
|
node.setAlpha(value.asFloat());
|
||||||
|
break;
|
||||||
|
case "visibility":
|
||||||
|
node.setVisibility(sVisibilityMapping.get(value.asString()));
|
||||||
|
break;
|
||||||
|
case "layoutParams":
|
||||||
|
setLayoutParams(node, Arrays.copyOfRange(path, 1, path.length), value);
|
||||||
|
break;
|
||||||
|
case "layoutDirection":
|
||||||
|
node.setLayoutDirection(sLayoutDirectionMapping.get(value.asString()));
|
||||||
|
break;
|
||||||
|
case "textDirection":
|
||||||
|
node.setTextDirection(sTextDirectionMapping.get(value.asString()));
|
||||||
|
break;
|
||||||
|
case "textAlignment":
|
||||||
|
node.setTextAlignment(sTextAlignmentMapping.get(value.asString()));
|
||||||
|
break;
|
||||||
|
case "background":
|
||||||
|
node.setBackground(new ColorDrawable(value.asInt()));
|
||||||
|
break;
|
||||||
|
case "foreground":
|
||||||
|
node.setForeground(new ColorDrawable(value.asInt()));
|
||||||
|
break;
|
||||||
|
case "state":
|
||||||
|
switch (path[2]) {
|
||||||
|
case "enabled":
|
||||||
|
node.setEnabled(value.asBoolean());
|
||||||
|
break;
|
||||||
|
case "activated":
|
||||||
|
node.setActivated(value.asBoolean());
|
||||||
|
break;
|
||||||
|
case "selected":
|
||||||
|
node.setSelected(value.asBoolean());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "bounds":
|
||||||
|
switch (path[2]) {
|
||||||
|
case "left":
|
||||||
|
node.setLeft(value.asInt());
|
||||||
|
break;
|
||||||
|
case "top":
|
||||||
|
node.setTop(value.asInt());
|
||||||
|
break;
|
||||||
|
case "right":
|
||||||
|
node.setRight(value.asInt());
|
||||||
|
break;
|
||||||
|
case "bottom":
|
||||||
|
node.setBottom(value.asInt());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "padding":
|
||||||
|
switch (path[2]) {
|
||||||
|
case "left":
|
||||||
|
node.setPadding(
|
||||||
|
value.asInt(),
|
||||||
|
node.getPaddingTop(),
|
||||||
|
node.getPaddingRight(),
|
||||||
|
node.getPaddingBottom());
|
||||||
|
break;
|
||||||
|
case "top":
|
||||||
|
node.setPadding(
|
||||||
|
node.getPaddingLeft(),
|
||||||
|
value.asInt(),
|
||||||
|
node.getPaddingRight(),
|
||||||
|
node.getPaddingBottom());
|
||||||
|
break;
|
||||||
|
case "right":
|
||||||
|
node.setPadding(
|
||||||
|
node.getPaddingLeft(),
|
||||||
|
node.getPaddingTop(),
|
||||||
|
value.asInt(),
|
||||||
|
node.getPaddingBottom());
|
||||||
|
break;
|
||||||
|
case "bottom":
|
||||||
|
node.setPadding(
|
||||||
|
node.getPaddingLeft(), node.getPaddingTop(), node.getPaddingRight(), value.asInt());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "rotation":
|
||||||
|
switch (path[2]) {
|
||||||
|
case "x":
|
||||||
|
node.setRotationX(value.asFloat());
|
||||||
|
break;
|
||||||
|
case "y":
|
||||||
|
node.setRotationY(value.asFloat());
|
||||||
|
break;
|
||||||
|
case "z":
|
||||||
|
node.setRotation(value.asFloat());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "translation":
|
||||||
|
switch (path[2]) {
|
||||||
|
case "x":
|
||||||
|
node.setTranslationX(value.asFloat());
|
||||||
|
break;
|
||||||
|
case "y":
|
||||||
|
node.setTranslationY(value.asFloat());
|
||||||
|
break;
|
||||||
|
case "z":
|
||||||
|
node.setTranslationZ(value.asFloat());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "position":
|
||||||
|
switch (path[2]) {
|
||||||
|
case "x":
|
||||||
|
node.setX(value.asFloat());
|
||||||
|
break;
|
||||||
|
case "y":
|
||||||
|
node.setY(value.asFloat());
|
||||||
|
break;
|
||||||
|
case "z":
|
||||||
|
node.setZ(value.asFloat());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "scale":
|
||||||
|
switch (path[2]) {
|
||||||
|
case "x":
|
||||||
|
node.setScaleX(value.asFloat());
|
||||||
|
break;
|
||||||
|
case "y":
|
||||||
|
node.setScaleY(value.asFloat());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "pivot":
|
||||||
|
switch (path[2]) {
|
||||||
|
case "x":
|
||||||
|
node.setPivotY(value.asFloat());
|
||||||
|
break;
|
||||||
|
case "y":
|
||||||
|
node.setPivotX(value.asFloat());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "width":
|
||||||
|
LayoutParams lpw = node.getLayoutParams();
|
||||||
|
lpw.width = value.asInt();
|
||||||
|
node.setLayoutParams(lpw);
|
||||||
|
break;
|
||||||
|
case "height":
|
||||||
|
LayoutParams lph = node.getLayoutParams();
|
||||||
|
lph.height = value.asInt();
|
||||||
|
node.setLayoutParams(lph);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
invalidate(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setAccessibilityValue(View node, String[] path, SonarDynamic value) {
|
||||||
|
switch (path[1]) {
|
||||||
|
case "focusable":
|
||||||
|
node.setFocusable(value.asBoolean());
|
||||||
|
break;
|
||||||
|
case "important-for-accessibility":
|
||||||
|
node.setImportantForAccessibility(
|
||||||
|
AccessibilityUtil.sImportantForAccessibilityMapping.get(value.asString()));
|
||||||
|
break;
|
||||||
|
case "content-description":
|
||||||
|
node.setContentDescription(value.asString());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
invalidate(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Named<String>> getAttributes(View node) throws Exception {
|
||||||
|
final List<Named<String>> attributes = new ArrayList<>();
|
||||||
|
|
||||||
|
final String resourceId = getResourceId(node);
|
||||||
|
if (resourceId != null) {
|
||||||
|
attributes.add(new Named<>("id", resourceId));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sListenerInfoField != null && sOnClickListenerField != null) {
|
||||||
|
final Object listenerInfo = sListenerInfoField.get(node);
|
||||||
|
if (listenerInfo != null) {
|
||||||
|
final OnClickListener clickListener =
|
||||||
|
(OnClickListener) sOnClickListenerField.get(listenerInfo);
|
||||||
|
if (clickListener != null) {
|
||||||
|
attributes.add(new Named<>("onClick", clickListener.getClass().getName()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return attributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private static String getResourceId(View node) {
|
||||||
|
final int id = node.getId();
|
||||||
|
|
||||||
|
if (id == View.NO_ID) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResourcesUtil.getIdStringQuietly(node.getContext(), node.getResources(), id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setHighlighted(View node, boolean selected) {
|
||||||
|
// We need to figure out whether the given View has a parent View since margins are not
|
||||||
|
// included within a View's bounds. So, in order to display the margin values for a particular
|
||||||
|
// view, we need to apply an overlay on its parent rather than itself.
|
||||||
|
final View targetView;
|
||||||
|
final ViewParent parent = node.getParent();
|
||||||
|
if (parent instanceof View) {
|
||||||
|
targetView = (View) parent;
|
||||||
|
} else {
|
||||||
|
targetView = node;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selected) {
|
||||||
|
HighlightedOverlay.removeHighlight(targetView);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Rect padding =
|
||||||
|
new Rect(
|
||||||
|
ViewCompat.getPaddingStart(node),
|
||||||
|
node.getPaddingTop(),
|
||||||
|
ViewCompat.getPaddingEnd(node),
|
||||||
|
node.getPaddingBottom());
|
||||||
|
|
||||||
|
final Rect margin;
|
||||||
|
final ViewGroup.LayoutParams params = node.getLayoutParams();
|
||||||
|
if (params instanceof ViewGroup.MarginLayoutParams) {
|
||||||
|
final ViewGroup.MarginLayoutParams marginParams = (ViewGroup.MarginLayoutParams) params;
|
||||||
|
margin =
|
||||||
|
new Rect(
|
||||||
|
MarginLayoutParamsCompat.getMarginStart(marginParams),
|
||||||
|
marginParams.topMargin,
|
||||||
|
MarginLayoutParamsCompat.getMarginEnd(marginParams),
|
||||||
|
marginParams.bottomMargin);
|
||||||
|
} else {
|
||||||
|
margin = new Rect();
|
||||||
|
}
|
||||||
|
|
||||||
|
final int left = node.getLeft();
|
||||||
|
final int top = node.getTop();
|
||||||
|
final Rect contentBounds = new Rect(left, top, left + node.getWidth(), top + node.getHeight());
|
||||||
|
if (targetView == node) {
|
||||||
|
// If the View doesn't have a parent View that we're applying the overlay to, then
|
||||||
|
// we need to ensure that it is aligned to 0, 0, rather than its relative location to its
|
||||||
|
// parent
|
||||||
|
contentBounds.offset(-left, -top);
|
||||||
|
}
|
||||||
|
|
||||||
|
HighlightedOverlay.setHighlighted(targetView, margin, padding, contentBounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void hitTest(View node, Touch touch) {
|
||||||
|
touch.finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable String getDecoration(View obj) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean matches(String query, View node) throws Exception {
|
||||||
|
final String resourceId = getResourceId(node);
|
||||||
|
|
||||||
|
if (resourceId != null && resourceId.toLowerCase().contains(query)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
final NodeDescriptor objectDescriptor = descriptorForClass(Object.class);
|
||||||
|
return objectDescriptor.matches(query, node);
|
||||||
|
}
|
||||||
|
|
||||||
|
private SonarObject getTags(final View node) {
|
||||||
|
final SonarObject.Builder tags = new SonarObject.Builder();
|
||||||
|
if (sKeyedTagsField == null) {
|
||||||
|
return tags.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
new ErrorReportingRunnable() {
|
||||||
|
@Override
|
||||||
|
protected void runOrThrow() throws Exception {
|
||||||
|
final SparseArray keyedTags = (SparseArray) sKeyedTagsField.get(node);
|
||||||
|
if (keyedTags != null) {
|
||||||
|
for (int i = 0, count = keyedTags.size(); i < count; i++) {
|
||||||
|
final String id =
|
||||||
|
ResourcesUtil.getIdStringQuietly(
|
||||||
|
node.getContext(), node.getResources(), keyedTags.keyAt(i));
|
||||||
|
tags.put(id, keyedTags.valueAt(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.run();
|
||||||
|
|
||||||
|
return tags.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static InspectorValue fromDrawable(Drawable d) {
|
||||||
|
if (d instanceof ColorDrawable) {
|
||||||
|
return InspectorValue.mutable(Color, ((ColorDrawable) d).getColor());
|
||||||
|
}
|
||||||
|
return InspectorValue.mutable(Color, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SonarObject getLayoutParams(View node) {
|
||||||
|
final LayoutParams layoutParams = node.getLayoutParams();
|
||||||
|
final SonarObject.Builder params = new SonarObject.Builder();
|
||||||
|
|
||||||
|
params.put("width", fromSize(layoutParams.width));
|
||||||
|
params.put("height", fromSize(layoutParams.height));
|
||||||
|
|
||||||
|
if (layoutParams instanceof MarginLayoutParams) {
|
||||||
|
final MarginLayoutParams marginLayoutParams = (MarginLayoutParams) layoutParams;
|
||||||
|
params.put(
|
||||||
|
"margin",
|
||||||
|
new SonarObject.Builder()
|
||||||
|
.put("left", InspectorValue.mutable(marginLayoutParams.leftMargin))
|
||||||
|
.put("top", InspectorValue.mutable(marginLayoutParams.topMargin))
|
||||||
|
.put("right", InspectorValue.mutable(marginLayoutParams.rightMargin))
|
||||||
|
.put("bottom", InspectorValue.mutable(marginLayoutParams.bottomMargin)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layoutParams instanceof FrameLayout.LayoutParams) {
|
||||||
|
final FrameLayout.LayoutParams frameLayoutParams = (FrameLayout.LayoutParams) layoutParams;
|
||||||
|
params.put("gravity", sGravityMapping.get(frameLayoutParams.gravity));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layoutParams instanceof LinearLayout.LayoutParams) {
|
||||||
|
final LinearLayout.LayoutParams linearLayoutParams = (LinearLayout.LayoutParams) layoutParams;
|
||||||
|
params
|
||||||
|
.put("weight", InspectorValue.mutable(linearLayoutParams.weight))
|
||||||
|
.put("gravity", sGravityMapping.get(linearLayoutParams.gravity));
|
||||||
|
}
|
||||||
|
|
||||||
|
return params.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setLayoutParams(View node, String[] path, SonarDynamic value) {
|
||||||
|
final LayoutParams params = node.getLayoutParams();
|
||||||
|
|
||||||
|
switch (path[0]) {
|
||||||
|
case "width":
|
||||||
|
params.width = toSize(value.asString());
|
||||||
|
break;
|
||||||
|
case "height":
|
||||||
|
params.height = toSize(value.asString());
|
||||||
|
break;
|
||||||
|
case "weight":
|
||||||
|
final LinearLayout.LayoutParams linearParams = (LinearLayout.LayoutParams) params;
|
||||||
|
linearParams.weight = value.asFloat();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params instanceof MarginLayoutParams) {
|
||||||
|
final MarginLayoutParams marginParams = (MarginLayoutParams) params;
|
||||||
|
|
||||||
|
switch (path[0]) {
|
||||||
|
case "margin":
|
||||||
|
switch (path[1]) {
|
||||||
|
case "left":
|
||||||
|
marginParams.leftMargin = value.asInt();
|
||||||
|
break;
|
||||||
|
case "top":
|
||||||
|
marginParams.topMargin = value.asInt();
|
||||||
|
break;
|
||||||
|
case "right":
|
||||||
|
marginParams.rightMargin = value.asInt();
|
||||||
|
break;
|
||||||
|
case "bottom":
|
||||||
|
marginParams.bottomMargin = value.asInt();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params instanceof FrameLayout.LayoutParams) {
|
||||||
|
final FrameLayout.LayoutParams frameLayoutParams = (FrameLayout.LayoutParams) params;
|
||||||
|
|
||||||
|
switch (path[0]) {
|
||||||
|
case "gravity":
|
||||||
|
frameLayoutParams.gravity = sGravityMapping.get(value.asString());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params instanceof LinearLayout.LayoutParams) {
|
||||||
|
final LinearLayout.LayoutParams linearParams = (LinearLayout.LayoutParams) params;
|
||||||
|
|
||||||
|
switch (path[0]) {
|
||||||
|
case "weight":
|
||||||
|
linearParams.weight = value.asFloat();
|
||||||
|
break;
|
||||||
|
case "gravity":
|
||||||
|
linearParams.gravity = sGravityMapping.get(value.asString());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
node.setLayoutParams(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static InspectorValue fromSize(int size) {
|
||||||
|
switch (size) {
|
||||||
|
case LayoutParams.WRAP_CONTENT:
|
||||||
|
return InspectorValue.mutable(Enum, "WRAP_CONTENT");
|
||||||
|
case LayoutParams.MATCH_PARENT:
|
||||||
|
return InspectorValue.mutable(Enum, "MATCH_PARENT");
|
||||||
|
default:
|
||||||
|
return InspectorValue.mutable(Enum, Integer.toString(size));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int toSize(String size) {
|
||||||
|
switch (size) {
|
||||||
|
case "WRAP_CONTENT":
|
||||||
|
return LayoutParams.WRAP_CONTENT;
|
||||||
|
case "MATCH_PARENT":
|
||||||
|
return LayoutParams.MATCH_PARENT;
|
||||||
|
default:
|
||||||
|
return Integer.parseInt(size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final EnumMapping sVisibilityMapping =
|
||||||
|
new EnumMapping("VISIBLE") {
|
||||||
|
{
|
||||||
|
put("VISIBLE", View.VISIBLE);
|
||||||
|
put("INVISIBLE", View.INVISIBLE);
|
||||||
|
put("GONE", View.GONE);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private static final EnumMapping sLayoutDirectionMapping =
|
||||||
|
new EnumMapping("LAYOUT_DIRECTION_INHERIT") {
|
||||||
|
{
|
||||||
|
put("LAYOUT_DIRECTION_INHERIT", View.LAYOUT_DIRECTION_INHERIT);
|
||||||
|
put("LAYOUT_DIRECTION_LOCALE", View.LAYOUT_DIRECTION_LOCALE);
|
||||||
|
put("LAYOUT_DIRECTION_LTR", View.LAYOUT_DIRECTION_LTR);
|
||||||
|
put("LAYOUT_DIRECTION_RTL", View.LAYOUT_DIRECTION_RTL);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private static final EnumMapping sTextDirectionMapping =
|
||||||
|
new EnumMapping("TEXT_DIRECTION_INHERIT") {
|
||||||
|
{
|
||||||
|
put("TEXT_DIRECTION_INHERIT", View.TEXT_DIRECTION_INHERIT);
|
||||||
|
put("TEXT_DIRECTION_FIRST_STRONG", View.TEXT_DIRECTION_FIRST_STRONG);
|
||||||
|
put("TEXT_DIRECTION_ANY_RTL", View.TEXT_DIRECTION_ANY_RTL);
|
||||||
|
put("TEXT_DIRECTION_LTR", View.TEXT_DIRECTION_LTR);
|
||||||
|
put("TEXT_DIRECTION_RTL", View.TEXT_DIRECTION_RTL);
|
||||||
|
put("TEXT_DIRECTION_LOCALE", View.TEXT_DIRECTION_LOCALE);
|
||||||
|
put("TEXT_DIRECTION_FIRST_STRONG_LTR", View.TEXT_DIRECTION_FIRST_STRONG_LTR);
|
||||||
|
put("TEXT_DIRECTION_FIRST_STRONG_RTL", View.TEXT_DIRECTION_FIRST_STRONG_RTL);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private static final EnumMapping sTextAlignmentMapping =
|
||||||
|
new EnumMapping("TEXT_ALIGNMENT_INHERIT") {
|
||||||
|
{
|
||||||
|
put("TEXT_ALIGNMENT_INHERIT", View.TEXT_ALIGNMENT_INHERIT);
|
||||||
|
put("TEXT_ALIGNMENT_GRAVITY", View.TEXT_ALIGNMENT_GRAVITY);
|
||||||
|
put("TEXT_ALIGNMENT_TEXT_START", View.TEXT_ALIGNMENT_TEXT_START);
|
||||||
|
put("TEXT_ALIGNMENT_TEXT_END", View.TEXT_ALIGNMENT_TEXT_END);
|
||||||
|
put("TEXT_ALIGNMENT_CENTER", View.TEXT_ALIGNMENT_CENTER);
|
||||||
|
put("TEXT_ALIGNMENT_VIEW_START", View.TEXT_ALIGNMENT_VIEW_START);
|
||||||
|
put("TEXT_ALIGNMENT_VIEW_END", View.TEXT_ALIGNMENT_VIEW_END);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private static final EnumMapping sGravityMapping =
|
||||||
|
new EnumMapping("NO_GRAVITY") {
|
||||||
|
{
|
||||||
|
put("NO_GRAVITY", Gravity.NO_GRAVITY);
|
||||||
|
put("LEFT", Gravity.LEFT);
|
||||||
|
put("TOP", Gravity.TOP);
|
||||||
|
put("RIGHT", Gravity.RIGHT);
|
||||||
|
put("BOTTOM", Gravity.BOTTOM);
|
||||||
|
put("CENTER", Gravity.CENTER);
|
||||||
|
put("CENTER_VERTICAL", Gravity.CENTER_VERTICAL);
|
||||||
|
put("FILL_VERTICAL", Gravity.FILL_VERTICAL);
|
||||||
|
put("CENTER_HORIZONTAL", Gravity.CENTER_HORIZONTAL);
|
||||||
|
put("FILL_HORIZONTAL", Gravity.FILL_HORIZONTAL);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
273
android/plugins/inspector/descriptors/ViewGroupDescriptor.java
Normal file
273
android/plugins/inspector/descriptors/ViewGroupDescriptor.java
Normal file
@@ -0,0 +1,273 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* 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.sonar.plugins.inspector.descriptors;
|
||||||
|
|
||||||
|
import static android.support.v4.view.ViewGroupCompat.LAYOUT_MODE_CLIP_BOUNDS;
|
||||||
|
import static android.support.v4.view.ViewGroupCompat.LAYOUT_MODE_OPTICAL_BOUNDS;
|
||||||
|
import static com.facebook.sonar.plugins.inspector.InspectorValue.Type.Boolean;
|
||||||
|
import static com.facebook.sonar.plugins.inspector.InspectorValue.Type.Enum;
|
||||||
|
|
||||||
|
import android.os.Build;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import com.facebook.sonar.R;
|
||||||
|
import com.facebook.sonar.core.SonarDynamic;
|
||||||
|
import com.facebook.sonar.core.SonarObject;
|
||||||
|
import com.facebook.sonar.plugins.inspector.HiddenNode;
|
||||||
|
import com.facebook.sonar.plugins.inspector.InspectorValue;
|
||||||
|
import com.facebook.sonar.plugins.inspector.Named;
|
||||||
|
import com.facebook.sonar.plugins.inspector.NodeDescriptor;
|
||||||
|
import com.facebook.sonar.plugins.inspector.Touch;
|
||||||
|
import com.facebook.stetho.common.android.FragmentCompatUtil;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
public class ViewGroupDescriptor extends NodeDescriptor<ViewGroup> {
|
||||||
|
|
||||||
|
private class NodeKey {
|
||||||
|
private int[] mKey;
|
||||||
|
|
||||||
|
boolean set(ViewGroup node) {
|
||||||
|
final int childCount = node.getChildCount();
|
||||||
|
final int[] key = new int[childCount];
|
||||||
|
|
||||||
|
for (int i = 0; i < childCount; i++) {
|
||||||
|
final View child = node.getChildAt(i);
|
||||||
|
key[i] = System.identityHashCode(child);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean changed = false;
|
||||||
|
if (mKey == null) {
|
||||||
|
changed = true;
|
||||||
|
} else if (mKey.length != key.length) {
|
||||||
|
changed = true;
|
||||||
|
} else {
|
||||||
|
for (int i = 0; i < childCount; i++) {
|
||||||
|
if (mKey[i] != key[i]) {
|
||||||
|
changed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mKey = key;
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(final ViewGroup node) {
|
||||||
|
final NodeKey key = new NodeKey();
|
||||||
|
|
||||||
|
final Runnable maybeInvalidate =
|
||||||
|
new ErrorReportingRunnable() {
|
||||||
|
@Override
|
||||||
|
public void runOrThrow() throws Exception {
|
||||||
|
if (connected()) {
|
||||||
|
if (key.set(node)) {
|
||||||
|
invalidate(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
final boolean hasAttachedToWindow =
|
||||||
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
|
||||||
|
if (!hasAttachedToWindow || node.isAttachedToWindow()) {
|
||||||
|
node.postDelayed(this, 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
node.postDelayed(maybeInvalidate, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId(ViewGroup node) throws Exception {
|
||||||
|
NodeDescriptor descriptor = descriptorForClass(View.class);
|
||||||
|
return descriptor.getId(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName(ViewGroup node) throws Exception {
|
||||||
|
NodeDescriptor descriptor = descriptorForClass(View.class);
|
||||||
|
return descriptor.getName(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getChildCount(ViewGroup node) {
|
||||||
|
int childCount = 0;
|
||||||
|
for (int i = 0, count = node.getChildCount(); i < count; i++) {
|
||||||
|
if (!(node.getChildAt(i) instanceof HiddenNode)) {
|
||||||
|
childCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return childCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable Object getChildAt(ViewGroup node, int index) {
|
||||||
|
for (int i = 0, count = node.getChildCount(); i < count; i++) {
|
||||||
|
final View child = node.getChildAt(i);
|
||||||
|
if (child instanceof HiddenNode) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i >= index) {
|
||||||
|
final Object fragment = getAttachedFragmentForView(child);
|
||||||
|
if (fragment != null && !FragmentCompatUtil.isDialogFragment(fragment)) {
|
||||||
|
return fragment;
|
||||||
|
}
|
||||||
|
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Named<SonarObject>> getData(ViewGroup node) throws Exception {
|
||||||
|
final List<Named<SonarObject>> props = new ArrayList<>();
|
||||||
|
final NodeDescriptor descriptor = descriptorForClass(View.class);
|
||||||
|
|
||||||
|
final SonarObject.Builder vgProps = new SonarObject.Builder();
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
||||||
|
vgProps
|
||||||
|
.put(
|
||||||
|
"layoutMode",
|
||||||
|
InspectorValue.mutable(
|
||||||
|
Enum,
|
||||||
|
node.getLayoutMode() == LAYOUT_MODE_CLIP_BOUNDS
|
||||||
|
? "LAYOUT_MODE_CLIP_BOUNDS"
|
||||||
|
: "LAYOUT_MODE_OPTICAL_BOUNDS"))
|
||||||
|
.put("clipChildren", InspectorValue.mutable(Boolean, node.getClipChildren()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
vgProps.put("clipToPadding", InspectorValue.mutable(Boolean, node.getClipToPadding()));
|
||||||
|
}
|
||||||
|
|
||||||
|
props.add(0, new Named<>("ViewGroup", vgProps.build()));
|
||||||
|
|
||||||
|
props.addAll(descriptor.getData(node));
|
||||||
|
|
||||||
|
return props;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setValue(ViewGroup node, String[] path, SonarDynamic value) throws Exception {
|
||||||
|
switch (path[0]) {
|
||||||
|
case "ViewGroup":
|
||||||
|
switch (path[1]) {
|
||||||
|
case "layoutMode":
|
||||||
|
switch (value.asString()) {
|
||||||
|
case "LAYOUT_MODE_CLIP_BOUNDS":
|
||||||
|
node.setLayoutMode(LAYOUT_MODE_CLIP_BOUNDS);
|
||||||
|
break;
|
||||||
|
case "LAYOUT_MODE_OPTICAL_BOUNDS":
|
||||||
|
node.setLayoutMode(LAYOUT_MODE_OPTICAL_BOUNDS);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
node.setLayoutMode(-1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "clipChildren":
|
||||||
|
node.setClipChildren(value.asBoolean());
|
||||||
|
break;
|
||||||
|
case "clipToPadding":
|
||||||
|
node.setClipToPadding(value.asBoolean());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
final NodeDescriptor descriptor = descriptorForClass(View.class);
|
||||||
|
descriptor.setValue(node, path, value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
invalidate(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Named<String>> getAttributes(ViewGroup node) throws Exception {
|
||||||
|
final NodeDescriptor descriptor = descriptorForClass(View.class);
|
||||||
|
return descriptor.getAttributes(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setHighlighted(ViewGroup node, boolean selected) throws Exception {
|
||||||
|
final NodeDescriptor descriptor = descriptorForClass(View.class);
|
||||||
|
descriptor.setHighlighted(node, selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void hitTest(ViewGroup node, Touch touch) {
|
||||||
|
for (int i = node.getChildCount() - 1; i >= 0; i--) {
|
||||||
|
final View child = node.getChildAt(i);
|
||||||
|
if (child instanceof HiddenNode
|
||||||
|
|| child.getVisibility() != View.VISIBLE
|
||||||
|
|| shouldSkip(child)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int scrollX = node.getScrollX();
|
||||||
|
final int scrollY = node.getScrollY();
|
||||||
|
|
||||||
|
final int left = (child.getLeft() + (int) child.getTranslationX()) - scrollX;
|
||||||
|
final int top = (child.getTop() + (int) child.getTranslationY()) - scrollY;
|
||||||
|
final int right = (child.getRight() + (int) child.getTranslationX()) - scrollX;
|
||||||
|
final int bottom = (child.getBottom() + (int) child.getTranslationY()) - scrollY;
|
||||||
|
|
||||||
|
final boolean hit = touch.containedIn(left, top, right, bottom);
|
||||||
|
|
||||||
|
if (hit) {
|
||||||
|
touch.continueWithOffset(i, left, top);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
touch.finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean shouldSkip(View view) {
|
||||||
|
Object tag = view.getTag(R.id.sonar_skip_view_traversal);
|
||||||
|
if (!(tag instanceof Boolean)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (Boolean) tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable String getDecoration(ViewGroup obj) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean matches(String query, ViewGroup node) throws Exception {
|
||||||
|
final NodeDescriptor descriptor = descriptorForClass(Object.class);
|
||||||
|
return descriptor.matches(query, node);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Object getAttachedFragmentForView(View v) {
|
||||||
|
try {
|
||||||
|
final Object fragment = FragmentCompatUtil.findFragmentForView(v);
|
||||||
|
boolean added = false;
|
||||||
|
if (fragment instanceof android.app.Fragment) {
|
||||||
|
added = ((android.app.Fragment) fragment).isAdded();
|
||||||
|
} else if (fragment instanceof android.support.v4.app.Fragment) {
|
||||||
|
added = ((android.support.v4.app.Fragment) fragment).isAdded();
|
||||||
|
}
|
||||||
|
|
||||||
|
return added ? fragment : null;
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
81
android/plugins/inspector/descriptors/WindowDescriptor.java
Normal file
81
android/plugins/inspector/descriptors/WindowDescriptor.java
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* 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.sonar.plugins.inspector.descriptors;
|
||||||
|
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.Window;
|
||||||
|
import com.facebook.sonar.core.SonarDynamic;
|
||||||
|
import com.facebook.sonar.core.SonarObject;
|
||||||
|
import com.facebook.sonar.plugins.inspector.Named;
|
||||||
|
import com.facebook.sonar.plugins.inspector.NodeDescriptor;
|
||||||
|
import com.facebook.sonar.plugins.inspector.Touch;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
public class WindowDescriptor extends NodeDescriptor<Window> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(Window node) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId(Window node) {
|
||||||
|
return Integer.toString(System.identityHashCode(node));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName(Window node) {
|
||||||
|
return node.getClass().getSimpleName();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getChildCount(Window node) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getChildAt(Window node, int index) {
|
||||||
|
return node.getDecorView();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Named<SonarObject>> getData(Window node) {
|
||||||
|
return Collections.EMPTY_LIST;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setValue(Window node, String[] path, SonarDynamic value) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Named<String>> getAttributes(Window node) {
|
||||||
|
return Collections.EMPTY_LIST;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setHighlighted(Window node, boolean selected) throws Exception {
|
||||||
|
final NodeDescriptor descriptor = descriptorForClass(View.class);
|
||||||
|
descriptor.setHighlighted(node.getDecorView(), selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void hitTest(Window node, Touch touch) {
|
||||||
|
touch.continueWithOffset(0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable String getDecoration(Window obj) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean matches(String query, Window node) throws Exception {
|
||||||
|
final NodeDescriptor descriptor = descriptorForClass(Object.class);
|
||||||
|
return descriptor.matches(query, node);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,383 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* 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.sonar.plugins.inspector.descriptors.utils;
|
||||||
|
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import android.support.v4.view.ViewCompat;
|
||||||
|
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.ViewParent;
|
||||||
|
import com.facebook.sonar.plugins.inspector.descriptors.utils.AccessibilityRoleUtil.AccessibilityRole;
|
||||||
|
import java.util.List;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 class AccessibilityEvaluationUtil {
|
||||||
|
|
||||||
|
private AccessibilityEvaluationUtil() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
||||||
|
return node != null
|
||||||
|
&& node.getCollectionInfo() == null
|
||||||
|
&& (!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;
|
||||||
|
}
|
||||||
|
|
||||||
|
final 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.
|
||||||
|
*
|
||||||
|
* <p>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;
|
||||||
|
}
|
||||||
|
|
||||||
|
final ViewGroup viewGroup = (ViewGroup) view;
|
||||||
|
for (int i = 0, count = viewGroup.getChildCount(); i < count; i++) {
|
||||||
|
final View childView = viewGroup.getChildAt(i);
|
||||||
|
|
||||||
|
if (childView == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
final AccessibilityNodeInfoCompat childNode = AccessibilityNodeInfoCompat.obtain();
|
||||||
|
try {
|
||||||
|
ViewCompat.onInitializeAccessibilityNodeInfo(childView, childNode);
|
||||||
|
|
||||||
|
if (!node.isVisibleToUser()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAccessibilityFocusable(childNode, childView)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSpeakingNode(childNode, childView)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (childNode != null) {
|
||||||
|
childNode.recycle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if the provided {@link View} and {@link AccessibilityNodeInfoCompat} meet the
|
||||||
|
* criteria for gaining accessibility focus.
|
||||||
|
*
|
||||||
|
* <p>Note: this is evaluating general focusability by accessibility services, and does not mean
|
||||||
|
* this view will be guaranteed to be focused by specific services such as Talkback. For Talkback
|
||||||
|
* focusability, see {@link #isTalkbackFocusable(View)}
|
||||||
|
*
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
|
||||||
|
final View parent = (View) ViewCompat.getParentForAccessibility(view);
|
||||||
|
if (parent == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.isScrollable()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
final List actionList = node.getActionList();
|
||||||
|
if (actionList.contains(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD)
|
||||||
|
|| actionList.contains(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Top-level items in a scrolling pager are actually two levels down since the first
|
||||||
|
// level items in pagers are the pages themselves.
|
||||||
|
View grandparent = (View) ViewCompat.getParentForAccessibility(parent);
|
||||||
|
if (grandparent != null
|
||||||
|
&& AccessibilityRoleUtil.getRole(grandparent) == AccessibilityRole.PAGER) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
AccessibilityRole parentRole = AccessibilityRoleUtil.getRole(parent);
|
||||||
|
return parentRole == AccessibilityRole.LIST
|
||||||
|
|| parentRole == AccessibilityRole.GRID
|
||||||
|
|| parentRole == AccessibilityRole.SCROLL_VIEW
|
||||||
|
|| parentRole == AccessibilityRole.HORIZONTAL_SCROLL_VIEW;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
final 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
final ViewParent parentView = ViewCompat.getParentForAccessibility(view);
|
||||||
|
if (!(parentView instanceof View)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final AccessibilityNodeInfoCompat parentNode = AccessibilityNodeInfoCompat.obtain();
|
||||||
|
try {
|
||||||
|
ViewCompat.onInitializeAccessibilityNodeInfo((View) parentView, parentNode);
|
||||||
|
if (parentNode == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasEqualBoundsToViewRoot(parentNode, (View) parentView)
|
||||||
|
&& parentNode.getChildCount() > 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAccessibilityFocusable(parentNode, (View) parentView)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasFocusableAncestor(parentNode, (View) parentView)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
parentNode.recycle();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether a one given view is a descendant of another.
|
||||||
|
*
|
||||||
|
* @param view The {@link View} to evaluate
|
||||||
|
* @param potentialAncestor The potential ancestor {@link View}
|
||||||
|
* @return {@code true} if view is a descendant of potentialAncestor
|
||||||
|
*/
|
||||||
|
private static boolean viewIsDescendant(View view, View potentialAncestor) {
|
||||||
|
ViewParent parent = view.getParent();
|
||||||
|
while (parent != null) {
|
||||||
|
if (parent == potentialAncestor) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
parent = parent.getParent();
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether a View has the same size and position as its View Root.
|
||||||
|
*
|
||||||
|
* @param view The {@link View} to evaluate
|
||||||
|
* @return {@code true} if view has equal bounds
|
||||||
|
*/
|
||||||
|
public static boolean hasEqualBoundsToViewRoot(AccessibilityNodeInfoCompat node, View view) {
|
||||||
|
AndroidRootResolver rootResolver = new AndroidRootResolver();
|
||||||
|
List<AndroidRootResolver.Root> roots = rootResolver.listActiveRoots();
|
||||||
|
if (roots != null) {
|
||||||
|
for (AndroidRootResolver.Root root : roots) {
|
||||||
|
if (view == root.view) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viewIsDescendant(view, root.view)) {
|
||||||
|
Rect nodeBounds = new Rect();
|
||||||
|
node.getBoundsInScreen(nodeBounds);
|
||||||
|
|
||||||
|
Rect viewRootBounds = new Rect();
|
||||||
|
viewRootBounds.set(
|
||||||
|
root.param.x,
|
||||||
|
root.param.y,
|
||||||
|
root.param.width + root.param.x,
|
||||||
|
root.param.height + root.param.y);
|
||||||
|
|
||||||
|
return nodeBounds.equals(viewRootBounds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether a given {@link View} will be focusable by Google's TalkBack screen reader.
|
||||||
|
*
|
||||||
|
* @param view The {@link View} to evaluate.
|
||||||
|
* @return {@code boolean} if the view will be ignored by TalkBack.
|
||||||
|
*/
|
||||||
|
public static boolean isTalkbackFocusable(View view) {
|
||||||
|
if (view == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int important = ViewCompat.getImportantForAccessibility(view);
|
||||||
|
if (important == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO
|
||||||
|
|| important == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 false;
|
||||||
|
}
|
||||||
|
parent = parent.getParent();
|
||||||
|
}
|
||||||
|
|
||||||
|
final AccessibilityNodeInfoCompat node = ViewAccessibilityHelper.createNodeInfoFromView(view);
|
||||||
|
if (node == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-leaf nodes identical in size to their View Root should not be focusable.
|
||||||
|
if (hasEqualBoundsToViewRoot(node, view) && node.getChildCount() > 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!node.isVisibleToUser()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (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 true;
|
||||||
|
} else if (isSpeakingNode(node, view)) {
|
||||||
|
// Node is focusable and has something to speak
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Node is focusable and has nothing to speak
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if view is not accessibility focusable, it needs to have text and no focusable ancestors.
|
||||||
|
if (!hasText(node)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasFocusableAncestor(node, view)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
node.recycle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* 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.sonar.plugins.inspector.descriptors.utils;
|
||||||
|
|
||||||
|
import android.support.v4.view.ViewCompat;
|
||||||
|
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.accessibility.AccessibilityNodeInfo;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility class that handles the addition of a "role" for accessibility to either a View or
|
||||||
|
* AccessibilityNodeInfo.
|
||||||
|
*/
|
||||||
|
public class AccessibilityRoleUtil {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* These roles are defined by Google's TalkBack screen reader, and this list should be kept up to
|
||||||
|
* date with their implementation. Details can be seen in their source code here:
|
||||||
|
*
|
||||||
|
* <p>https://github.com/google/talkback/blob/master/utils/src/main/java/Role.java
|
||||||
|
*/
|
||||||
|
public enum AccessibilityRole {
|
||||||
|
NONE(null),
|
||||||
|
BUTTON("android.widget.Button"),
|
||||||
|
CHECK_BOX("android.widget.CompoundButton"),
|
||||||
|
DROP_DOWN_LIST("android.widget.Spinner"),
|
||||||
|
EDIT_TEXT("android.widget.EditText"),
|
||||||
|
GRID("android.widget.GridView"),
|
||||||
|
IMAGE("android.widget.ImageView"),
|
||||||
|
IMAGE_BUTTON("android.widget.ImageView"),
|
||||||
|
LIST("android.widget.AbsListView"),
|
||||||
|
PAGER("android.support.v4.view.ViewPager"),
|
||||||
|
RADIO_BUTTON("android.widget.RadioButton"),
|
||||||
|
SEEK_CONTROL("android.widget.SeekBar"),
|
||||||
|
SWITCH("android.widget.Switch"),
|
||||||
|
TAB_BAR("android.widget.TabWidget"),
|
||||||
|
TOGGLE_BUTTON("android.widget.ToggleButton"),
|
||||||
|
VIEW_GROUP("android.view.ViewGroup"),
|
||||||
|
WEB_VIEW("android.webkit.WebView"),
|
||||||
|
CHECKED_TEXT_VIEW("android.widget.CheckedTextView"),
|
||||||
|
PROGRESS_BAR("android.widget.ProgressBar"),
|
||||||
|
ACTION_BAR_TAB("android.app.ActionBar$Tab"),
|
||||||
|
DRAWER_LAYOUT("android.support.v4.widget.DrawerLayout"),
|
||||||
|
SLIDING_DRAWER("android.widget.SlidingDrawer"),
|
||||||
|
ICON_MENU("com.android.internal.view.menu.IconMenuView"),
|
||||||
|
TOAST("android.widget.Toast$TN"),
|
||||||
|
DATE_PICKER_DIALOG("android.app.DatePickerDialog"),
|
||||||
|
TIME_PICKER_DIALOG("android.app.TimePickerDialog"),
|
||||||
|
DATE_PICKER("android.widget.DatePicker"),
|
||||||
|
TIME_PICKER("android.widget.TimePicker"),
|
||||||
|
NUMBER_PICKER("android.widget.NumberPicker"),
|
||||||
|
SCROLL_VIEW("android.widget.ScrollView"),
|
||||||
|
HORIZONTAL_SCROLL_VIEW("android.widget.HorizontalScrollView"),
|
||||||
|
KEYBOARD_KEY("android.inputmethodservice.Keyboard$Key");
|
||||||
|
|
||||||
|
@Nullable private final String mValue;
|
||||||
|
|
||||||
|
AccessibilityRole(String type) {
|
||||||
|
mValue = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public String getValue() {
|
||||||
|
return mValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AccessibilityRole fromValue(String value) {
|
||||||
|
for (AccessibilityRole role : AccessibilityRole.values()) {
|
||||||
|
if (role.getValue() != null && role.getValue().equals(value)) {
|
||||||
|
return role;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return AccessibilityRole.NONE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private AccessibilityRoleUtil() {
|
||||||
|
// No instances
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AccessibilityRole getRole(View view) {
|
||||||
|
AccessibilityNodeInfoCompat nodeInfo = AccessibilityNodeInfoCompat.obtain();
|
||||||
|
ViewCompat.onInitializeAccessibilityNodeInfo(view, nodeInfo);
|
||||||
|
AccessibilityRole role = getRole(nodeInfo);
|
||||||
|
nodeInfo.recycle();
|
||||||
|
return role;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AccessibilityRole getRole(AccessibilityNodeInfo nodeInfo) {
|
||||||
|
return getRole(new AccessibilityNodeInfoCompat(nodeInfo));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AccessibilityRole getRole(AccessibilityNodeInfoCompat nodeInfo) {
|
||||||
|
AccessibilityRole role = AccessibilityRole.fromValue((String) nodeInfo.getClassName());
|
||||||
|
if (role.equals(AccessibilityRole.IMAGE_BUTTON) || role.equals(AccessibilityRole.IMAGE)) {
|
||||||
|
return nodeInfo.isClickable() ? AccessibilityRole.IMAGE_BUTTON : AccessibilityRole.IMAGE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (role.equals(AccessibilityRole.NONE)) {
|
||||||
|
AccessibilityNodeInfoCompat.CollectionInfoCompat collection = nodeInfo.getCollectionInfo();
|
||||||
|
if (collection != null) {
|
||||||
|
// RecyclerView will be classified as a list or grid.
|
||||||
|
if (collection.getRowCount() > 1 && collection.getColumnCount() > 1) {
|
||||||
|
return AccessibilityRole.GRID;
|
||||||
|
} else {
|
||||||
|
return AccessibilityRole.LIST;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return role;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,357 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* 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.sonar.plugins.inspector.descriptors.utils;
|
||||||
|
|
||||||
|
import static android.content.Context.ACCESSIBILITY_SERVICE;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.support.v4.view.ViewCompat;
|
||||||
|
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.ViewParent;
|
||||||
|
import android.view.accessibility.AccessibilityManager;
|
||||||
|
import android.widget.EditText;
|
||||||
|
import com.facebook.sonar.core.SonarArray;
|
||||||
|
import com.facebook.sonar.core.SonarObject;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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() {}
|
||||||
|
|
||||||
|
public static final EnumMapping sAccessibilityActionMapping =
|
||||||
|
new EnumMapping("UNKNOWN") {
|
||||||
|
{
|
||||||
|
put("FOCUS", AccessibilityNodeInfoCompat.ACTION_FOCUS);
|
||||||
|
put("CLEAR_FOCUS", AccessibilityNodeInfoCompat.ACTION_CLEAR_FOCUS);
|
||||||
|
put("SELECT", AccessibilityNodeInfoCompat.ACTION_SELECT);
|
||||||
|
put("CLEAR_SELECTION", AccessibilityNodeInfoCompat.ACTION_CLEAR_SELECTION);
|
||||||
|
put("CLICK", AccessibilityNodeInfoCompat.ACTION_CLICK);
|
||||||
|
put("LONG_CLICK", AccessibilityNodeInfoCompat.ACTION_LONG_CLICK);
|
||||||
|
put("ACCESSIBILITY_FOCUS", AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS);
|
||||||
|
put(
|
||||||
|
"CLEAR_ACCESSIBILITY_FOCUS",
|
||||||
|
AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
|
||||||
|
put(
|
||||||
|
"NEXT_AT_MOVEMENT_GRANULARITY",
|
||||||
|
AccessibilityNodeInfoCompat.ACTION_NEXT_AT_MOVEMENT_GRANULARITY);
|
||||||
|
put(
|
||||||
|
"PREVIOUS_AT_MOVEMENT_GRANULARITY",
|
||||||
|
AccessibilityNodeInfoCompat.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY);
|
||||||
|
put("NEXT_HTML_ELEMENT", AccessibilityNodeInfoCompat.ACTION_NEXT_HTML_ELEMENT);
|
||||||
|
put("PREVIOUS_HTML_ELEMENT", AccessibilityNodeInfoCompat.ACTION_PREVIOUS_HTML_ELEMENT);
|
||||||
|
put("SCROLL_FORWARD", AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD);
|
||||||
|
put("SCROLL_BACKWARD", AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD);
|
||||||
|
put("CUT", AccessibilityNodeInfoCompat.ACTION_CUT);
|
||||||
|
put("COPY", AccessibilityNodeInfoCompat.ACTION_COPY);
|
||||||
|
put("PASTE", AccessibilityNodeInfoCompat.ACTION_PASTE);
|
||||||
|
put("SET_SELECTION", AccessibilityNodeInfoCompat.ACTION_SET_SELECTION);
|
||||||
|
put("SET_SELECTION", AccessibilityNodeInfoCompat.ACTION_SET_SELECTION);
|
||||||
|
put("EXPAND", AccessibilityNodeInfoCompat.ACTION_EXPAND);
|
||||||
|
put("COLLAPSE", AccessibilityNodeInfoCompat.ACTION_COLLAPSE);
|
||||||
|
put("DISMISS", AccessibilityNodeInfoCompat.ACTION_DISMISS);
|
||||||
|
put("SET_TEXT", AccessibilityNodeInfoCompat.ACTION_SET_TEXT);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public static final EnumMapping sImportantForAccessibilityMapping =
|
||||||
|
new EnumMapping("AUTO") {
|
||||||
|
{
|
||||||
|
put("AUTO", View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
|
||||||
|
put("NO", View.IMPORTANT_FOR_ACCESSIBILITY_NO);
|
||||||
|
put("YES", View.IMPORTANT_FOR_ACCESSIBILITY_YES);
|
||||||
|
put("NO_HIDE_DESCENDANTS", View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a {@link Context}, determine if any accessibility service is running.
|
||||||
|
*
|
||||||
|
* @param context The {@link Context} used to get the {@link AccessibilityManager}.
|
||||||
|
* @return {@code true} if an accessibility service is currently running.
|
||||||
|
*/
|
||||||
|
public static boolean isAccessibilityEnabled(Context context) {
|
||||||
|
return ((AccessibilityManager) context.getSystemService(ACCESSIBILITY_SERVICE)).isEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a sentence describing why a given {@link View} will be ignored by Google's TalkBack
|
||||||
|
* screen reader.
|
||||||
|
*
|
||||||
|
* @param view The {@link View} to evaluate.
|
||||||
|
* @return {@code String} describing why a {@link View} is ignored.
|
||||||
|
*/
|
||||||
|
public static String getTalkbackIgnoredReasons(View view) {
|
||||||
|
final 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
final AccessibilityNodeInfoCompat node = ViewAccessibilityHelper.createNodeInfoFromView(view);
|
||||||
|
if (node == null) {
|
||||||
|
return "AccessibilityNodeInfo cannot be found.";
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (AccessibilityEvaluationUtil.hasEqualBoundsToViewRoot(node, view)) {
|
||||||
|
return "View has the same dimensions as the View Root.";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!node.isVisibleToUser()) {
|
||||||
|
return "View is not visible.";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (AccessibilityEvaluationUtil.isAccessibilityFocusable(node, view)) {
|
||||||
|
return "View is actionable, but has no description.";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (AccessibilityEvaluationUtil.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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a sentence describing why a given {@link View} will be focusable by Google's TalkBack
|
||||||
|
* screen reader.
|
||||||
|
*
|
||||||
|
* @param view The {@link View} to evaluate.
|
||||||
|
* @return {@code String} describing why a {@link View} is focusable.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public static String getTalkbackFocusableReasons(View view) {
|
||||||
|
final AccessibilityNodeInfoCompat node = ViewAccessibilityHelper.createNodeInfoFromView(view);
|
||||||
|
if (node == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final boolean hasText = AccessibilityEvaluationUtil.hasText(node);
|
||||||
|
final boolean isCheckable = node.isCheckable();
|
||||||
|
final boolean hasNonActionableSpeakingDescendants =
|
||||||
|
AccessibilityEvaluationUtil.hasNonActionableSpeakingDescendants(node, view);
|
||||||
|
|
||||||
|
if (AccessibilityEvaluationUtil.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 (AccessibilityEvaluationUtil.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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the text that Gogole's TalkBack screen reader will read aloud for a given {@link View}.
|
||||||
|
* This may be any combination of the {@link View}'s {@code text}, {@code contentDescription}, and
|
||||||
|
* the {@code text} and {@code contentDescription} of any ancestor {@link View}.
|
||||||
|
*
|
||||||
|
* <p>Note: This string does not include any additional semantic information that Talkback will
|
||||||
|
* read, such as "Button", or "disabled".
|
||||||
|
*
|
||||||
|
* @param view The {@link View} to evaluate.
|
||||||
|
* @return {@code String} describing why a {@link View} is focusable.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public static CharSequence getTalkbackDescription(View view) {
|
||||||
|
final AccessibilityNodeInfoCompat node = ViewAccessibilityHelper.createNodeInfoFromView(view);
|
||||||
|
if (node == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final CharSequence contentDescription = node.getContentDescription();
|
||||||
|
final CharSequence nodeText = node.getText();
|
||||||
|
|
||||||
|
final boolean hasNodeText = !TextUtils.isEmpty(nodeText);
|
||||||
|
final 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 = ", ";
|
||||||
|
final ViewGroup viewGroup = (ViewGroup) view;
|
||||||
|
|
||||||
|
for (int i = 0, count = viewGroup.getChildCount(); i < count; i++) {
|
||||||
|
final View child = viewGroup.getChildAt(i);
|
||||||
|
|
||||||
|
final AccessibilityNodeInfoCompat childNodeInfo = AccessibilityNodeInfoCompat.obtain();
|
||||||
|
ViewCompat.onInitializeAccessibilityNodeInfo(child, childNodeInfo);
|
||||||
|
|
||||||
|
CharSequence childNodeDescription = null;
|
||||||
|
if (AccessibilityEvaluationUtil.isSpeakingNode(childNodeInfo, child)
|
||||||
|
&& !AccessibilityEvaluationUtil.isAccessibilityFocusable(childNodeInfo, child)) {
|
||||||
|
childNodeDescription = getTalkbackDescription(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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a {@link SonarObject} of useful properties of AccessibilityNodeInfo, to be shown in the
|
||||||
|
* Sonar Layout Inspector. All properties are immutable since they are all derived from various
|
||||||
|
* {@link View} properties.
|
||||||
|
*
|
||||||
|
* @param view The {@link View} to derive the AccessibilityNodeInfo properties from.
|
||||||
|
* @return {@link SonarObject} containing the properties.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public static SonarObject getAccessibilityNodeInfoProperties(View view) {
|
||||||
|
final AccessibilityNodeInfoCompat nodeInfo =
|
||||||
|
ViewAccessibilityHelper.createNodeInfoFromView(view);
|
||||||
|
if (nodeInfo == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final SonarObject.Builder nodeInfoProps = new SonarObject.Builder();
|
||||||
|
final Rect bounds = new Rect();
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
final SonarArray.Builder actionsArrayBuilder = new SonarArray.Builder();
|
||||||
|
for (AccessibilityNodeInfoCompat.AccessibilityActionCompat action :
|
||||||
|
nodeInfo.getActionList()) {
|
||||||
|
final String actionLabel = (String) action.getLabel();
|
||||||
|
if (actionLabel != null) {
|
||||||
|
actionsArrayBuilder.put(actionLabel);
|
||||||
|
} else {
|
||||||
|
actionsArrayBuilder.put(
|
||||||
|
AccessibilityUtil.sAccessibilityActionMapping.get(action.getId(), false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nodeInfoProps.put("actions", actionsArrayBuilder.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
nodeInfoProps
|
||||||
|
.put("clickable", nodeInfo.isClickable())
|
||||||
|
.put("content-description", nodeInfo.getContentDescription())
|
||||||
|
.put("text", nodeInfo.getText())
|
||||||
|
.put("focused", nodeInfo.isAccessibilityFocused())
|
||||||
|
.put("long-clickable", nodeInfo.isLongClickable())
|
||||||
|
.put("focusable", nodeInfo.isFocusable());
|
||||||
|
|
||||||
|
nodeInfo.getBoundsInParent(bounds);
|
||||||
|
nodeInfoProps.put(
|
||||||
|
"parent-bounds",
|
||||||
|
new SonarObject.Builder()
|
||||||
|
.put("width", bounds.width())
|
||||||
|
.put("height", bounds.height())
|
||||||
|
.put("top", bounds.top)
|
||||||
|
.put("left", bounds.left)
|
||||||
|
.put("bottom", bounds.bottom)
|
||||||
|
.put("right", bounds.right));
|
||||||
|
|
||||||
|
nodeInfo.getBoundsInScreen(bounds);
|
||||||
|
nodeInfoProps.put(
|
||||||
|
"screen-bounds",
|
||||||
|
new SonarObject.Builder()
|
||||||
|
.put("width", bounds.width())
|
||||||
|
.put("height", bounds.height())
|
||||||
|
.put("top", bounds.top)
|
||||||
|
.put("left", bounds.left)
|
||||||
|
.put("bottom", bounds.bottom)
|
||||||
|
.put("right", bounds.right));
|
||||||
|
|
||||||
|
nodeInfo.recycle();
|
||||||
|
|
||||||
|
return nodeInfoProps.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modifies a {@link SonarObject.Builder} to add Talkback-specific Accessibiltiy properties to be
|
||||||
|
* shown in the Sonar Layout Inspector.
|
||||||
|
*
|
||||||
|
* @param props The {@link SonarObject.Builder} to add the properties to.
|
||||||
|
* @param view The {@link View} to derive the properties from.
|
||||||
|
*/
|
||||||
|
public static void addTalkbackProperties(SonarObject.Builder props, View view) {
|
||||||
|
if (!AccessibilityEvaluationUtil.isTalkbackFocusable(view)) {
|
||||||
|
props
|
||||||
|
.put("talkback-ignored", true)
|
||||||
|
.put("talkback-ignored-reasons", getTalkbackIgnoredReasons(view));
|
||||||
|
} else {
|
||||||
|
props
|
||||||
|
.put("talkback-focusable", true)
|
||||||
|
.put("talkback-focusable-reasons", getTalkbackFocusableReasons(view))
|
||||||
|
.put("talkback-description", getTalkbackDescription(view));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* 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.sonar.plugins.inspector.descriptors.utils;
|
||||||
|
|
||||||
|
import static android.view.WindowManager.LayoutParams;
|
||||||
|
|
||||||
|
import android.os.Build;
|
||||||
|
import android.view.View;
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
public final class AndroidRootResolver {
|
||||||
|
|
||||||
|
private static final String WINDOW_MANAGER_IMPL_CLAZZ = "android.view.WindowManagerImpl";
|
||||||
|
private static final String WINDOW_MANAGER_GLOBAL_CLAZZ = "android.view.WindowManagerGlobal";
|
||||||
|
private static final String VIEWS_FIELD = "mViews";
|
||||||
|
private static final String WINDOW_PARAMS_FIELD = "mParams";
|
||||||
|
private static final String GET_DEFAULT_IMPL = "getDefault";
|
||||||
|
private static final String GET_GLOBAL_INSTANCE = "getInstance";
|
||||||
|
|
||||||
|
private boolean initialized;
|
||||||
|
private Object windowManagerObj;
|
||||||
|
private Field viewsField;
|
||||||
|
private Field paramsField;
|
||||||
|
|
||||||
|
public static class Root {
|
||||||
|
public final View view;
|
||||||
|
public final LayoutParams param;
|
||||||
|
|
||||||
|
private Root(View view, LayoutParams param) {
|
||||||
|
this.view = view;
|
||||||
|
this.param = param;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public @Nullable List<Root> listActiveRoots() {
|
||||||
|
if (!initialized) {
|
||||||
|
initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null == windowManagerObj) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null == viewsField) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (null == paramsField) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<View> views = null;
|
||||||
|
List<LayoutParams> params = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (Build.VERSION.SDK_INT < 19) {
|
||||||
|
views = Arrays.asList((View[]) viewsField.get(windowManagerObj));
|
||||||
|
params = Arrays.asList((LayoutParams[]) paramsField.get(windowManagerObj));
|
||||||
|
} else {
|
||||||
|
views = (List<View>) viewsField.get(windowManagerObj);
|
||||||
|
params = (List<LayoutParams>) paramsField.get(windowManagerObj);
|
||||||
|
}
|
||||||
|
} catch (RuntimeException | IllegalAccessException re) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Root> roots = new ArrayList<>();
|
||||||
|
for (int i = 0, stop = views.size(); i < stop; i++) {
|
||||||
|
roots.add(new Root(views.get(i), params.get(i)));
|
||||||
|
}
|
||||||
|
return roots;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initialize() {
|
||||||
|
initialized = true;
|
||||||
|
String accessClass =
|
||||||
|
Build.VERSION.SDK_INT > 16 ? WINDOW_MANAGER_GLOBAL_CLAZZ : WINDOW_MANAGER_IMPL_CLAZZ;
|
||||||
|
String instanceMethod = Build.VERSION.SDK_INT > 16 ? GET_GLOBAL_INSTANCE : GET_DEFAULT_IMPL;
|
||||||
|
|
||||||
|
try {
|
||||||
|
Class<?> clazz = Class.forName(accessClass);
|
||||||
|
Method getMethod = clazz.getMethod(instanceMethod);
|
||||||
|
windowManagerObj = getMethod.invoke(null);
|
||||||
|
viewsField = clazz.getDeclaredField(VIEWS_FIELD);
|
||||||
|
viewsField.setAccessible(true);
|
||||||
|
paramsField = clazz.getDeclaredField(WINDOW_PARAMS_FIELD);
|
||||||
|
paramsField.setAccessible(true);
|
||||||
|
} catch (InvocationTargetException | IllegalAccessException | RuntimeException | NoSuchMethodException | NoSuchFieldException | ClassNotFoundException ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
51
android/plugins/inspector/descriptors/utils/EnumMapping.java
Normal file
51
android/plugins/inspector/descriptors/utils/EnumMapping.java
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* 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.sonar.plugins.inspector.descriptors.utils;
|
||||||
|
|
||||||
|
import static com.facebook.sonar.plugins.inspector.InspectorValue.Type.Enum;
|
||||||
|
|
||||||
|
import android.support.v4.util.SimpleArrayMap;
|
||||||
|
import com.facebook.sonar.plugins.inspector.InspectorValue;
|
||||||
|
|
||||||
|
public class EnumMapping {
|
||||||
|
private final SimpleArrayMap<String, Integer> mMapping = new SimpleArrayMap<>();
|
||||||
|
private final String mDefaultKey;
|
||||||
|
|
||||||
|
public EnumMapping(String defaultKey) {
|
||||||
|
mDefaultKey = defaultKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void put(String s, int i) {
|
||||||
|
mMapping.put(s, i);
|
||||||
|
}
|
||||||
|
|
||||||
|
public InspectorValue get(final int i) {
|
||||||
|
return get(i, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public InspectorValue get(final int i, final boolean mutable) {
|
||||||
|
for (int ii = 0, count = mMapping.size(); ii < count; ii++) {
|
||||||
|
if (mMapping.valueAt(ii) == i) {
|
||||||
|
return mutable
|
||||||
|
? InspectorValue.mutable(Enum, mMapping.keyAt(ii))
|
||||||
|
: InspectorValue.immutable(Enum, mMapping.keyAt(ii));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mutable
|
||||||
|
? InspectorValue.mutable(Enum, mDefaultKey)
|
||||||
|
: InspectorValue.immutable(Enum, mDefaultKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int get(String s) {
|
||||||
|
if (mMapping.containsKey(s)) {
|
||||||
|
return mMapping.get(s);
|
||||||
|
}
|
||||||
|
return mMapping.get(mDefaultKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* 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.sonar.plugins.inspector.descriptors.utils;
|
||||||
|
|
||||||
|
import android.support.v4.view.ViewCompat;
|
||||||
|
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
|
||||||
|
import android.view.View;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
/** Class that helps with accessibility by providing useful methods. */
|
||||||
|
public final class ViewAccessibilityHelper {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates and returns an {@link AccessibilityNodeInfoCompat} from the the provided {@link View}.
|
||||||
|
* Note: This does not handle recycling of the {@link AccessibilityNodeInfoCompat}.
|
||||||
|
*
|
||||||
|
* @param view The {@link View} to create the {@link AccessibilityNodeInfoCompat} from.
|
||||||
|
* @return {@link AccessibilityNodeInfoCompat}
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public static AccessibilityNodeInfoCompat createNodeInfoFromView(View view) {
|
||||||
|
if (view == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final AccessibilityNodeInfoCompat nodeInfo = AccessibilityNodeInfoCompat.obtain();
|
||||||
|
|
||||||
|
// For some unknown reason, Android seems to occasionally throw a NPE from
|
||||||
|
// onInitializeAccessibilityNodeInfo.
|
||||||
|
try {
|
||||||
|
ViewCompat.onInitializeAccessibilityNodeInfo(view, nodeInfo);
|
||||||
|
} catch (NullPointerException e) {
|
||||||
|
if (nodeInfo != null) {
|
||||||
|
nodeInfo.recycle();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return nodeInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,696 @@
|
|||||||
|
// Copyright 2004-present Facebook. All Rights Reserved.
|
||||||
|
|
||||||
|
package com.facebook.litho.sonar;
|
||||||
|
|
||||||
|
import static com.facebook.litho.annotations.ImportantForAccessibility.IMPORTANT_FOR_ACCESSIBILITY_NO;
|
||||||
|
import static com.facebook.litho.annotations.ImportantForAccessibility.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS;
|
||||||
|
import static com.facebook.sonar.plugins.inspector.InspectorValue.Type.Color;
|
||||||
|
import static com.facebook.sonar.plugins.inspector.InspectorValue.Type.Enum;
|
||||||
|
import static com.facebook.sonar.plugins.inspector.InspectorValue.Type.Number;
|
||||||
|
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import android.graphics.drawable.ColorDrawable;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.support.v4.util.Pair;
|
||||||
|
import android.view.View;
|
||||||
|
import com.facebook.litho.Component;
|
||||||
|
import com.facebook.litho.ComponentContext;
|
||||||
|
import com.facebook.litho.ComponentLifecycle;
|
||||||
|
import com.facebook.litho.DebugComponent;
|
||||||
|
import com.facebook.litho.DebugLayoutNode;
|
||||||
|
import com.facebook.litho.EventHandler;
|
||||||
|
import com.facebook.litho.LithoView;
|
||||||
|
import com.facebook.litho.annotations.Prop;
|
||||||
|
import com.facebook.litho.annotations.State;
|
||||||
|
import com.facebook.litho.reference.Reference;
|
||||||
|
import com.facebook.sonar.core.SonarDynamic;
|
||||||
|
import com.facebook.sonar.core.SonarObject;
|
||||||
|
import com.facebook.sonar.plugins.inspector.HighlightedOverlay;
|
||||||
|
import com.facebook.sonar.plugins.inspector.InspectorValue;
|
||||||
|
import com.facebook.sonar.plugins.inspector.Named;
|
||||||
|
import com.facebook.sonar.plugins.inspector.NodeDescriptor;
|
||||||
|
import com.facebook.sonar.plugins.inspector.Touch;
|
||||||
|
import com.facebook.sonar.plugins.inspector.descriptors.ObjectDescriptor;
|
||||||
|
import com.facebook.sonar.plugins.inspector.descriptors.utils.AccessibilityUtil;
|
||||||
|
import com.facebook.yoga.YogaAlign;
|
||||||
|
import com.facebook.yoga.YogaDirection;
|
||||||
|
import com.facebook.yoga.YogaEdge;
|
||||||
|
import com.facebook.yoga.YogaFlexDirection;
|
||||||
|
import com.facebook.yoga.YogaJustify;
|
||||||
|
import com.facebook.yoga.YogaPositionType;
|
||||||
|
import com.facebook.yoga.YogaValue;
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
public class DebugComponentDescriptor extends NodeDescriptor<DebugComponent> {
|
||||||
|
|
||||||
|
private Map<String, List<Pair<String[], SonarDynamic>>> mOverrides = new HashMap<>();
|
||||||
|
private DebugComponent.Overrider mOverrider =
|
||||||
|
new DebugComponent.Overrider() {
|
||||||
|
@Override
|
||||||
|
public void applyComponentOverrides(String key, Component component) {
|
||||||
|
final List<Pair<String[], SonarDynamic>> overrides = mOverrides.get(key);
|
||||||
|
if (overrides == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Pair<String[], SonarDynamic> override : overrides) {
|
||||||
|
if (override.first[0].equals("Props")) {
|
||||||
|
applyReflectiveOverride(component, override.first[1], override.second);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void applyStateOverrides(
|
||||||
|
String key, ComponentLifecycle.StateContainer stateContainer) {
|
||||||
|
final List<Pair<String[], SonarDynamic>> overrides = mOverrides.get(key);
|
||||||
|
if (overrides == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Pair<String[], SonarDynamic> override : overrides) {
|
||||||
|
if (override.first[0].equals("State")) {
|
||||||
|
applyReflectiveOverride(stateContainer, override.first[1], override.second);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void applyLayoutOverrides(String key, DebugLayoutNode node) {
|
||||||
|
final List<Pair<String[], SonarDynamic>> overrides = mOverrides.get(key);
|
||||||
|
if (overrides == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Pair<String[], SonarDynamic> override : overrides) {
|
||||||
|
if (override.first[0].equals("Layout")) {
|
||||||
|
try {
|
||||||
|
applyLayoutOverride(
|
||||||
|
node,
|
||||||
|
Arrays.copyOfRange(override.first, 1, override.first.length),
|
||||||
|
override.second);
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
} else if (override.first[0].equals("Accessibility")) {
|
||||||
|
applyAccessibilityOverride(node, override.first[1], override.second);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(DebugComponent node) {
|
||||||
|
// We rely on the LithoView being invalidated when a component hierarchy changes.
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId(DebugComponent node) {
|
||||||
|
return node.getGlobalKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName(DebugComponent node) throws Exception {
|
||||||
|
NodeDescriptor componentDescriptor = descriptorForClass(node.getComponent().getClass());
|
||||||
|
if (componentDescriptor.getClass() != ObjectDescriptor.class) {
|
||||||
|
return componentDescriptor.getName(node.getComponent());
|
||||||
|
}
|
||||||
|
return node.getComponent().getSimpleName();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getChildCount(DebugComponent node) {
|
||||||
|
if (node.getMountedView() != null || node.getMountedDrawable() != null) {
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
return node.getChildComponents().size();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getChildAt(DebugComponent node, int index) {
|
||||||
|
final View mountedView = node.getMountedView();
|
||||||
|
final Drawable mountedDrawable = node.getMountedDrawable();
|
||||||
|
|
||||||
|
if (mountedView != null) {
|
||||||
|
return mountedView;
|
||||||
|
} else if (mountedDrawable != null) {
|
||||||
|
return mountedDrawable;
|
||||||
|
} else {
|
||||||
|
return node.getChildComponents().get(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Named<SonarObject>> getData(DebugComponent node) throws Exception {
|
||||||
|
NodeDescriptor componentDescriptor = descriptorForClass(node.getComponent().getClass());
|
||||||
|
if (componentDescriptor.getClass() != ObjectDescriptor.class) {
|
||||||
|
return componentDescriptor.getData(node.getComponent());
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<Named<SonarObject>> data = new ArrayList<>();
|
||||||
|
|
||||||
|
final SonarObject layoutData = getLayoutData(node);
|
||||||
|
if (layoutData != null) {
|
||||||
|
data.add(new Named<>("Layout", layoutData));
|
||||||
|
}
|
||||||
|
|
||||||
|
final SonarObject propData = getPropData(node);
|
||||||
|
if (propData != null) {
|
||||||
|
data.add(new Named<>("Props", propData));
|
||||||
|
}
|
||||||
|
|
||||||
|
final SonarObject stateData = getStateData(node);
|
||||||
|
if (stateData != null) {
|
||||||
|
data.add(new Named<>("State", stateData));
|
||||||
|
}
|
||||||
|
|
||||||
|
final SonarObject accessibilityData = getAccessibilityData(node);
|
||||||
|
if (accessibilityData != null) {
|
||||||
|
data.add(new Named<>("Accessibility", accessibilityData));
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private static SonarObject getLayoutData(DebugComponent node) {
|
||||||
|
final DebugLayoutNode layout = node.getLayoutNode();
|
||||||
|
if (layout == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final SonarObject.Builder data = new SonarObject.Builder();
|
||||||
|
data.put("background", fromReference(node.getContext(), layout.getBackground()));
|
||||||
|
data.put("foreground", fromDrawable(layout.getForeground()));
|
||||||
|
|
||||||
|
data.put("direction", InspectorValue.mutable(Enum, layout.getLayoutDirection().toString()));
|
||||||
|
data.put("flex-direction", InspectorValue.mutable(Enum, layout.getFlexDirection().toString()));
|
||||||
|
data.put(
|
||||||
|
"justify-content", InspectorValue.mutable(Enum, layout.getJustifyContent().toString()));
|
||||||
|
data.put("align-items", InspectorValue.mutable(Enum, layout.getAlignItems().toString()));
|
||||||
|
data.put("align-self", InspectorValue.mutable(Enum, layout.getAlignSelf().toString()));
|
||||||
|
data.put("align-content", InspectorValue.mutable(Enum, layout.getAlignContent().toString()));
|
||||||
|
data.put("position-type", InspectorValue.mutable(Enum, layout.getPositionType().toString()));
|
||||||
|
|
||||||
|
data.put("flex-grow", fromFloat(layout.getFlexGrow()));
|
||||||
|
data.put("flex-shrink", fromFloat(layout.getFlexShrink()));
|
||||||
|
data.put("flex-basis", fromYogaValue(layout.getFlexBasis()));
|
||||||
|
|
||||||
|
data.put("width", fromYogaValue(layout.getWidth()));
|
||||||
|
data.put("min-width", fromYogaValue(layout.getMinWidth()));
|
||||||
|
data.put("max-width", fromYogaValue(layout.getMaxWidth()));
|
||||||
|
|
||||||
|
data.put("height", fromYogaValue(layout.getHeight()));
|
||||||
|
data.put("min-height", fromYogaValue(layout.getMinHeight()));
|
||||||
|
data.put("max-height", fromYogaValue(layout.getMaxHeight()));
|
||||||
|
|
||||||
|
data.put("aspect-ratio", fromFloat(layout.getAspectRatio()));
|
||||||
|
|
||||||
|
data.put(
|
||||||
|
"margin",
|
||||||
|
new SonarObject.Builder()
|
||||||
|
.put("left", fromYogaValue(layout.getMargin(YogaEdge.LEFT)))
|
||||||
|
.put("top", fromYogaValue(layout.getMargin(YogaEdge.TOP)))
|
||||||
|
.put("right", fromYogaValue(layout.getMargin(YogaEdge.RIGHT)))
|
||||||
|
.put("bottom", fromYogaValue(layout.getMargin(YogaEdge.BOTTOM)))
|
||||||
|
.put("start", fromYogaValue(layout.getMargin(YogaEdge.START)))
|
||||||
|
.put("end", fromYogaValue(layout.getMargin(YogaEdge.END)))
|
||||||
|
.put("horizontal", fromYogaValue(layout.getMargin(YogaEdge.HORIZONTAL)))
|
||||||
|
.put("vertical", fromYogaValue(layout.getMargin(YogaEdge.VERTICAL)))
|
||||||
|
.put("all", fromYogaValue(layout.getMargin(YogaEdge.ALL))));
|
||||||
|
|
||||||
|
data.put(
|
||||||
|
"padding",
|
||||||
|
new SonarObject.Builder()
|
||||||
|
.put("left", fromYogaValue(layout.getPadding(YogaEdge.LEFT)))
|
||||||
|
.put("top", fromYogaValue(layout.getPadding(YogaEdge.TOP)))
|
||||||
|
.put("right", fromYogaValue(layout.getPadding(YogaEdge.RIGHT)))
|
||||||
|
.put("bottom", fromYogaValue(layout.getPadding(YogaEdge.BOTTOM)))
|
||||||
|
.put("start", fromYogaValue(layout.getPadding(YogaEdge.START)))
|
||||||
|
.put("end", fromYogaValue(layout.getPadding(YogaEdge.END)))
|
||||||
|
.put("horizontal", fromYogaValue(layout.getPadding(YogaEdge.HORIZONTAL)))
|
||||||
|
.put("vertical", fromYogaValue(layout.getPadding(YogaEdge.VERTICAL)))
|
||||||
|
.put("all", fromYogaValue(layout.getPadding(YogaEdge.ALL))));
|
||||||
|
|
||||||
|
data.put(
|
||||||
|
"border",
|
||||||
|
new SonarObject.Builder()
|
||||||
|
.put("left", fromFloat(layout.getBorderWidth(YogaEdge.LEFT)))
|
||||||
|
.put("top", fromFloat(layout.getBorderWidth(YogaEdge.TOP)))
|
||||||
|
.put("right", fromFloat(layout.getBorderWidth(YogaEdge.RIGHT)))
|
||||||
|
.put("bottom", fromFloat(layout.getBorderWidth(YogaEdge.BOTTOM)))
|
||||||
|
.put("start", fromFloat(layout.getBorderWidth(YogaEdge.START)))
|
||||||
|
.put("end", fromFloat(layout.getBorderWidth(YogaEdge.END)))
|
||||||
|
.put("horizontal", fromFloat(layout.getBorderWidth(YogaEdge.HORIZONTAL)))
|
||||||
|
.put("vertical", fromFloat(layout.getBorderWidth(YogaEdge.VERTICAL)))
|
||||||
|
.put("all", fromFloat(layout.getBorderWidth(YogaEdge.ALL))));
|
||||||
|
|
||||||
|
data.put(
|
||||||
|
"position",
|
||||||
|
new SonarObject.Builder()
|
||||||
|
.put("left", fromYogaValue(layout.getPosition(YogaEdge.LEFT)))
|
||||||
|
.put("top", fromYogaValue(layout.getPosition(YogaEdge.TOP)))
|
||||||
|
.put("right", fromYogaValue(layout.getPosition(YogaEdge.RIGHT)))
|
||||||
|
.put("bottom", fromYogaValue(layout.getPosition(YogaEdge.BOTTOM)))
|
||||||
|
.put("start", fromYogaValue(layout.getPosition(YogaEdge.START)))
|
||||||
|
.put("end", fromYogaValue(layout.getPosition(YogaEdge.END)))
|
||||||
|
.put("horizontal", fromYogaValue(layout.getPosition(YogaEdge.HORIZONTAL)))
|
||||||
|
.put("vertical", fromYogaValue(layout.getPosition(YogaEdge.VERTICAL)))
|
||||||
|
.put("all", fromYogaValue(layout.getPosition(YogaEdge.ALL))));
|
||||||
|
|
||||||
|
return data.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private static SonarObject getPropData(DebugComponent node) {
|
||||||
|
if (node.isInternalComponent()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Component component = node.getComponent();
|
||||||
|
final SonarObject.Builder props = new SonarObject.Builder();
|
||||||
|
|
||||||
|
boolean hasProps = false;
|
||||||
|
for (Field f : component.getClass().getDeclaredFields()) {
|
||||||
|
try {
|
||||||
|
f.setAccessible(true);
|
||||||
|
|
||||||
|
final Prop annotation = f.getAnnotation(Prop.class);
|
||||||
|
if (annotation != null) {
|
||||||
|
switch (annotation.resType()) {
|
||||||
|
case COLOR:
|
||||||
|
props.put(f.getName(), fromColor((Integer) f.get(component)));
|
||||||
|
break;
|
||||||
|
case DRAWABLE:
|
||||||
|
props.put(f.getName(), fromDrawable((Drawable) f.get(component)));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (f.get(component) != null
|
||||||
|
&& PropWithDescription.class.isAssignableFrom(f.get(component).getClass())) {
|
||||||
|
final Object description =
|
||||||
|
((PropWithDescription) f.get(component))
|
||||||
|
.getSonarLayoutInspectorPropDescription();
|
||||||
|
// Treat the description as immutable for now, because it's a "translation" of the
|
||||||
|
// actual prop,
|
||||||
|
// mutating them is not going to change the original prop.
|
||||||
|
if (description instanceof Map<?, ?>) {
|
||||||
|
final Map<?, ?> descriptionMap = (Map<?, ?>) description;
|
||||||
|
for (Map.Entry<?, ?> entry : descriptionMap.entrySet()) {
|
||||||
|
props.put(
|
||||||
|
entry.getKey().toString(), InspectorValue.immutable(entry.getValue()));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
props.put(f.getName(), InspectorValue.immutable(description));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (isTypeMutable(f.getType())) {
|
||||||
|
props.put(f.getName(), InspectorValue.mutable(f.get(component)));
|
||||||
|
} else {
|
||||||
|
props.put(f.getName(), InspectorValue.immutable(f.get(component)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
hasProps = true;
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasProps ? props.build() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private static SonarObject getStateData(DebugComponent node) {
|
||||||
|
if (node.isInternalComponent()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final ComponentLifecycle.StateContainer stateContainer = node.getStateContainer();
|
||||||
|
if (stateContainer == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final SonarObject.Builder state = new SonarObject.Builder();
|
||||||
|
|
||||||
|
boolean hasState = false;
|
||||||
|
for (Field f : stateContainer.getClass().getDeclaredFields()) {
|
||||||
|
try {
|
||||||
|
f.setAccessible(true);
|
||||||
|
|
||||||
|
final State annotation = f.getAnnotation(State.class);
|
||||||
|
if (annotation != null) {
|
||||||
|
if (isTypeMutable(f.getType())) {
|
||||||
|
state.put(f.getName(), InspectorValue.mutable(f.get(stateContainer)));
|
||||||
|
} else {
|
||||||
|
state.put(f.getName(), InspectorValue.immutable(f.get(stateContainer)));
|
||||||
|
}
|
||||||
|
hasState = true;
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasState ? state.build() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isTypeMutable(Class<?> type) {
|
||||||
|
if (type == int.class || type == Integer.class) {
|
||||||
|
return true;
|
||||||
|
} else if (type == long.class || type == Long.class) {
|
||||||
|
return true;
|
||||||
|
} else if (type == float.class || type == Float.class) {
|
||||||
|
return true;
|
||||||
|
} else if (type == double.class || type == Double.class) {
|
||||||
|
return true;
|
||||||
|
} else if (type == boolean.class || type == Boolean.class) {
|
||||||
|
return true;
|
||||||
|
} else if (type.isAssignableFrom(String.class)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private static SonarObject getAccessibilityData(DebugComponent node) {
|
||||||
|
final DebugLayoutNode layout = node.getLayoutNode();
|
||||||
|
if (layout == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final View hostView = node.getComponentHost();
|
||||||
|
final SonarObject.Builder accessibilityProps = new SonarObject.Builder();
|
||||||
|
|
||||||
|
// This needs to be an empty string to be mutable. See t20470623.
|
||||||
|
final CharSequence contentDescription =
|
||||||
|
layout.getContentDescription() != null ? layout.getContentDescription() : "";
|
||||||
|
accessibilityProps.put("content-description", InspectorValue.mutable(contentDescription));
|
||||||
|
accessibilityProps.put("focusable", InspectorValue.mutable(layout.getFocusable()));
|
||||||
|
accessibilityProps.put(
|
||||||
|
"important-for-accessibility",
|
||||||
|
AccessibilityUtil.sImportantForAccessibilityMapping.get(
|
||||||
|
layout.getImportantForAccessibility()));
|
||||||
|
|
||||||
|
// No host view exists, so this component is inherently not accessible. Add the reason why this
|
||||||
|
// is the case and then return.
|
||||||
|
if (hostView == node.getLithoView() || hostView == null) {
|
||||||
|
final int importantForAccessibility = layout.getImportantForAccessibility();
|
||||||
|
final boolean isAccessibilityEnabled =
|
||||||
|
AccessibilityUtil.isAccessibilityEnabled(node.getContext());
|
||||||
|
String ignoredReason;
|
||||||
|
|
||||||
|
if (!isAccessibilityEnabled) {
|
||||||
|
ignoredReason = "No accessibility service is running.";
|
||||||
|
} else if (importantForAccessibility == IMPORTANT_FOR_ACCESSIBILITY_NO) {
|
||||||
|
ignoredReason = "Component has importantForAccessibility set to NO.";
|
||||||
|
} else if (importantForAccessibility == IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS) {
|
||||||
|
ignoredReason = "Component has importantForAccessibility set to NO_HIDE_DESCENDANTS.";
|
||||||
|
} else {
|
||||||
|
ignoredReason = "Component does not have content, or accessibility handlers.";
|
||||||
|
}
|
||||||
|
|
||||||
|
accessibilityProps.put("talkback-ignored", true);
|
||||||
|
accessibilityProps.put("talkback-ignored-reasons", ignoredReason);
|
||||||
|
|
||||||
|
return accessibilityProps.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
accessibilityProps.put(
|
||||||
|
"node-info", AccessibilityUtil.getAccessibilityNodeInfoProperties(hostView));
|
||||||
|
AccessibilityUtil.addTalkbackProperties(accessibilityProps, hostView);
|
||||||
|
|
||||||
|
return accessibilityProps.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setValue(DebugComponent node, String[] path, SonarDynamic value) {
|
||||||
|
List<Pair<String[], SonarDynamic>> overrides = mOverrides.get(node.getGlobalKey());
|
||||||
|
if (overrides == null) {
|
||||||
|
overrides = new ArrayList<>();
|
||||||
|
mOverrides.put(node.getGlobalKey(), overrides);
|
||||||
|
}
|
||||||
|
overrides.add(new Pair<>(path, value));
|
||||||
|
|
||||||
|
node.setOverrider(mOverrider);
|
||||||
|
node.rerender();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Named<String>> getAttributes(DebugComponent node) {
|
||||||
|
final List<Named<String>> attributes = new ArrayList<>();
|
||||||
|
final String key = node.getKey();
|
||||||
|
final String testKey = node.getTestKey();
|
||||||
|
|
||||||
|
if (key != null && key.trim().length() > 0) {
|
||||||
|
attributes.add(new Named<>("key", key));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (testKey != null && testKey.trim().length() > 0) {
|
||||||
|
attributes.add(new Named<>("testKey", testKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
return attributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setHighlighted(DebugComponent node, boolean selected) {
|
||||||
|
final LithoView lithoView = node.getLithoView();
|
||||||
|
if (lithoView == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selected) {
|
||||||
|
HighlightedOverlay.removeHighlight(lithoView);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final DebugLayoutNode layout = node.getLayoutNode();
|
||||||
|
final boolean hasNode = layout != null;
|
||||||
|
final Rect margin;
|
||||||
|
if (!node.isRoot()) {
|
||||||
|
margin =
|
||||||
|
new Rect(
|
||||||
|
hasNode ? (int) layout.getResultMargin(YogaEdge.START) : 0,
|
||||||
|
hasNode ? (int) layout.getResultMargin(YogaEdge.TOP) : 0,
|
||||||
|
hasNode ? (int) layout.getResultMargin(YogaEdge.END) : 0,
|
||||||
|
hasNode ? (int) layout.getResultMargin(YogaEdge.BOTTOM) : 0);
|
||||||
|
} else {
|
||||||
|
// Margin not applied if you're at the root
|
||||||
|
margin = new Rect();
|
||||||
|
}
|
||||||
|
|
||||||
|
final Rect padding =
|
||||||
|
new Rect(
|
||||||
|
hasNode ? (int) layout.getResultPadding(YogaEdge.START) : 0,
|
||||||
|
hasNode ? (int) layout.getResultPadding(YogaEdge.TOP) : 0,
|
||||||
|
hasNode ? (int) layout.getResultPadding(YogaEdge.END) : 0,
|
||||||
|
hasNode ? (int) layout.getResultPadding(YogaEdge.BOTTOM) : 0);
|
||||||
|
|
||||||
|
final Rect contentBounds = node.getBoundsInLithoView();
|
||||||
|
HighlightedOverlay.setHighlighted(lithoView, margin, padding, contentBounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void hitTest(DebugComponent node, Touch touch) {
|
||||||
|
for (int i = getChildCount(node) - 1; i >= 0; i--) {
|
||||||
|
final Object child = getChildAt(node, i);
|
||||||
|
if (child instanceof DebugComponent) {
|
||||||
|
final DebugComponent componentChild = (DebugComponent) child;
|
||||||
|
final Rect bounds = componentChild.getBounds();
|
||||||
|
|
||||||
|
if (touch.containedIn(bounds.left, bounds.top, bounds.right, bounds.bottom)) {
|
||||||
|
touch.continueWithOffset(i, bounds.left, bounds.top);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (child instanceof View || child instanceof Drawable) {
|
||||||
|
// Components can only mount one view or drawable and its bounds are the same as the
|
||||||
|
// hosting component.
|
||||||
|
touch.continueWithOffset(i, 0, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
touch.finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDecoration(DebugComponent node) throws Exception {
|
||||||
|
if (node.getComponent() != null) {
|
||||||
|
NodeDescriptor componentDescriptor = descriptorForClass(node.getComponent().getClass());
|
||||||
|
if (componentDescriptor.getClass() != ObjectDescriptor.class) {
|
||||||
|
return componentDescriptor.getDecoration(node.getComponent());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "litho";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean matches(String query, DebugComponent node) throws Exception {
|
||||||
|
NodeDescriptor descriptor = descriptorForClass(Object.class);
|
||||||
|
return descriptor.matches(query, node);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void applyAccessibilityOverride(
|
||||||
|
DebugLayoutNode node, String key, SonarDynamic value) {
|
||||||
|
switch (key) {
|
||||||
|
case "focusable":
|
||||||
|
node.setFocusable(value.asBoolean());
|
||||||
|
break;
|
||||||
|
case "important-for-accessibility":
|
||||||
|
node.setImportantForAccessibility(
|
||||||
|
AccessibilityUtil.sImportantForAccessibilityMapping.get(value.asString()));
|
||||||
|
break;
|
||||||
|
case "content-description":
|
||||||
|
node.setContentDescription(value.asString());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void applyLayoutOverride(DebugLayoutNode node, String[] path, SonarDynamic value) {
|
||||||
|
switch (path[0]) {
|
||||||
|
case "background":
|
||||||
|
node.setBackgroundColor(value.asInt());
|
||||||
|
break;
|
||||||
|
case "foreground":
|
||||||
|
node.setForegroundColor(value.asInt());
|
||||||
|
break;
|
||||||
|
case "direction":
|
||||||
|
node.setLayoutDirection(YogaDirection.valueOf(value.asString().toUpperCase()));
|
||||||
|
break;
|
||||||
|
case "flex-direction":
|
||||||
|
node.setFlexDirection(YogaFlexDirection.valueOf(value.asString().toUpperCase()));
|
||||||
|
break;
|
||||||
|
case "justify-content":
|
||||||
|
node.setJustifyContent(YogaJustify.valueOf(value.asString().toUpperCase()));
|
||||||
|
break;
|
||||||
|
case "align-items":
|
||||||
|
node.setAlignItems(YogaAlign.valueOf(value.asString().toUpperCase()));
|
||||||
|
break;
|
||||||
|
case "align-self":
|
||||||
|
node.setAlignSelf(YogaAlign.valueOf(value.asString().toUpperCase()));
|
||||||
|
break;
|
||||||
|
case "align-content":
|
||||||
|
node.setAlignContent(YogaAlign.valueOf(value.asString().toUpperCase()));
|
||||||
|
break;
|
||||||
|
case "position-type":
|
||||||
|
node.setPositionType(YogaPositionType.valueOf(value.asString().toUpperCase()));
|
||||||
|
break;
|
||||||
|
case "flex-grow":
|
||||||
|
node.setFlexGrow(value.asFloat());
|
||||||
|
break;
|
||||||
|
case "flex-shrink":
|
||||||
|
node.setFlexShrink(value.asFloat());
|
||||||
|
break;
|
||||||
|
case "flex-basis":
|
||||||
|
node.setFlexBasis(YogaValue.parse(value.asString()));
|
||||||
|
break;
|
||||||
|
case "width":
|
||||||
|
node.setWidth(YogaValue.parse(value.asString()));
|
||||||
|
break;
|
||||||
|
case "min-width":
|
||||||
|
node.setMinWidth(YogaValue.parse(value.asString()));
|
||||||
|
break;
|
||||||
|
case "max-width":
|
||||||
|
node.setMaxWidth(YogaValue.parse(value.asString()));
|
||||||
|
break;
|
||||||
|
case "height":
|
||||||
|
node.setHeight(YogaValue.parse(value.asString()));
|
||||||
|
break;
|
||||||
|
case "min-height":
|
||||||
|
node.setMinHeight(YogaValue.parse(value.asString()));
|
||||||
|
break;
|
||||||
|
case "max-height":
|
||||||
|
node.setMaxHeight(YogaValue.parse(value.asString()));
|
||||||
|
break;
|
||||||
|
case "aspect-ratio":
|
||||||
|
node.setAspectRatio(value.asFloat());
|
||||||
|
break;
|
||||||
|
case "margin":
|
||||||
|
node.setMargin(edgeFromString(path[1]), YogaValue.parse(value.asString()));
|
||||||
|
break;
|
||||||
|
case "padding":
|
||||||
|
node.setPadding(edgeFromString(path[1]), YogaValue.parse(value.asString()));
|
||||||
|
break;
|
||||||
|
case "border":
|
||||||
|
node.setBorderWidth(edgeFromString(path[1]), value.asFloat());
|
||||||
|
break;
|
||||||
|
case "position":
|
||||||
|
node.setPosition(edgeFromString(path[1]), YogaValue.parse(value.asString()));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static YogaEdge edgeFromString(String s) {
|
||||||
|
return YogaEdge.valueOf(s.toUpperCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void applyReflectiveOverride(Object o, String key, SonarDynamic dynamic) {
|
||||||
|
try {
|
||||||
|
final Field field = o.getClass().getDeclaredField(key);
|
||||||
|
field.setAccessible(true);
|
||||||
|
|
||||||
|
final Class type = field.getType();
|
||||||
|
|
||||||
|
Object value = null;
|
||||||
|
if (type == int.class || type == Integer.class) {
|
||||||
|
value = dynamic.asInt();
|
||||||
|
} else if (type == long.class || type == Long.class) {
|
||||||
|
value = dynamic.asLong();
|
||||||
|
} else if (type == float.class || type == Float.class) {
|
||||||
|
value = dynamic.asFloat();
|
||||||
|
} else if (type == double.class || type == Double.class) {
|
||||||
|
value = dynamic.asDouble();
|
||||||
|
} else if (type == boolean.class || type == Boolean.class) {
|
||||||
|
value = dynamic.asBoolean();
|
||||||
|
} else if (type.isAssignableFrom(String.class)) {
|
||||||
|
value = dynamic.asString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value != null) {
|
||||||
|
field.set(o, value);
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static InspectorValue fromDrawable(Drawable d) {
|
||||||
|
if (d instanceof ColorDrawable) {
|
||||||
|
return InspectorValue.mutable(Color, ((ColorDrawable) d).getColor());
|
||||||
|
}
|
||||||
|
return InspectorValue.mutable(Color, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T extends Drawable> InspectorValue fromReference(
|
||||||
|
ComponentContext c, Reference<T> r) {
|
||||||
|
if (r == null) {
|
||||||
|
return fromDrawable(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
final T d = Reference.acquire(c, r);
|
||||||
|
final InspectorValue v = fromDrawable(d);
|
||||||
|
Reference.release(c, d, r);
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static InspectorValue fromFloat(float f) {
|
||||||
|
if (Float.isNaN(f)) {
|
||||||
|
return InspectorValue.mutable(Enum, "undefined");
|
||||||
|
}
|
||||||
|
return InspectorValue.mutable(Number, f);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static InspectorValue fromYogaValue(YogaValue v) {
|
||||||
|
// TODO add support for Type.Dimension or similar
|
||||||
|
return InspectorValue.mutable(Enum, v.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static InspectorValue fromColor(int color) {
|
||||||
|
return InspectorValue.mutable(Color, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
// Copyright 2004-present Facebook. All Rights Reserved.
|
||||||
|
|
||||||
|
package com.facebook.litho.sonar;
|
||||||
|
|
||||||
|
import com.facebook.litho.DebugComponent;
|
||||||
|
import com.facebook.litho.LithoView;
|
||||||
|
import com.facebook.sonar.plugins.inspector.DescriptorMapping;
|
||||||
|
|
||||||
|
public final class LithoSonarDescriptors {
|
||||||
|
|
||||||
|
public static void add(DescriptorMapping descriptorMapping) {
|
||||||
|
descriptorMapping.register(LithoView.class, new LithoViewDescriptor());
|
||||||
|
descriptorMapping.register(DebugComponent.class, new DebugComponentDescriptor());
|
||||||
|
}
|
||||||
|
}
|
||||||
111
android/plugins/inspector/litho-sonar/LithoViewDescriptor.java
Normal file
111
android/plugins/inspector/litho-sonar/LithoViewDescriptor.java
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
// Copyright 2004-present Facebook. All Rights Reserved.
|
||||||
|
|
||||||
|
package com.facebook.litho.sonar;
|
||||||
|
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import com.facebook.litho.DebugComponent;
|
||||||
|
import com.facebook.litho.LithoView;
|
||||||
|
import com.facebook.sonar.core.SonarDynamic;
|
||||||
|
import com.facebook.sonar.core.SonarObject;
|
||||||
|
import com.facebook.sonar.plugins.inspector.Named;
|
||||||
|
import com.facebook.sonar.plugins.inspector.NodeDescriptor;
|
||||||
|
import com.facebook.sonar.plugins.inspector.Touch;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class LithoViewDescriptor extends NodeDescriptor<LithoView> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(LithoView node) throws Exception {
|
||||||
|
node.setOnDirtyMountListener(
|
||||||
|
new LithoView.OnDirtyMountListener() {
|
||||||
|
@Override
|
||||||
|
public void onDirtyMount(LithoView view) {
|
||||||
|
invalidate(view);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId(LithoView node) throws Exception {
|
||||||
|
final NodeDescriptor descriptor = descriptorForClass(ViewGroup.class);
|
||||||
|
return descriptor.getId(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName(LithoView node) throws Exception {
|
||||||
|
final NodeDescriptor descriptor = descriptorForClass(ViewGroup.class);
|
||||||
|
return descriptor.getName(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getChildCount(LithoView node) {
|
||||||
|
return DebugComponent.getRootInstance(node) == null ? 0 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getChildAt(LithoView node, int index) {
|
||||||
|
return DebugComponent.getRootInstance(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Named<SonarObject>> getData(LithoView node) throws Exception {
|
||||||
|
final List<Named<SonarObject>> props = new ArrayList<>();
|
||||||
|
final NodeDescriptor descriptor = descriptorForClass(ViewGroup.class);
|
||||||
|
final Rect mountedBounds = node.getPreviousMountBounds();
|
||||||
|
|
||||||
|
props.add(
|
||||||
|
0,
|
||||||
|
new Named<>(
|
||||||
|
"LithoView",
|
||||||
|
new SonarObject.Builder()
|
||||||
|
.put(
|
||||||
|
"mountbounds",
|
||||||
|
new SonarObject.Builder()
|
||||||
|
.put("left", mountedBounds.left)
|
||||||
|
.put("top", mountedBounds.top)
|
||||||
|
.put("right", mountedBounds.right)
|
||||||
|
.put("bottom", mountedBounds.bottom))
|
||||||
|
.build()));
|
||||||
|
|
||||||
|
props.addAll(descriptor.getData(node));
|
||||||
|
|
||||||
|
return props;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setValue(LithoView node, String[] path, SonarDynamic value) throws Exception {
|
||||||
|
final NodeDescriptor descriptor = descriptorForClass(ViewGroup.class);
|
||||||
|
descriptor.setValue(node, path, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Named<String>> getAttributes(LithoView node) throws Exception {
|
||||||
|
final NodeDescriptor descriptor = descriptorForClass(ViewGroup.class);
|
||||||
|
return descriptor.getAttributes(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setHighlighted(LithoView node, boolean selected) throws Exception {
|
||||||
|
final NodeDescriptor descriptor = descriptorForClass(ViewGroup.class);
|
||||||
|
descriptor.setHighlighted(node, selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void hitTest(LithoView node, Touch touch) {
|
||||||
|
touch.continueWithOffset(0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDecoration(LithoView node) throws Exception {
|
||||||
|
final NodeDescriptor descriptor = descriptorForClass(ViewGroup.class);
|
||||||
|
return descriptor.getDecoration(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean matches(String query, LithoView node) throws Exception {
|
||||||
|
NodeDescriptor descriptor = descriptorForClass(Object.class);
|
||||||
|
return descriptor.matches(query, node);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
// Copyright 2004-present Facebook. All Rights Reserved.
|
||||||
|
|
||||||
|
package com.facebook.litho.sonar;
|
||||||
|
|
||||||
|
public interface PropWithDescription {
|
||||||
|
|
||||||
|
Object getSonarLayoutInspectorPropDescription();
|
||||||
|
}
|
||||||
69
android/plugins/network/NetworkReporter.java
Normal file
69
android/plugins/network/NetworkReporter.java
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* 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.sonar.plugins.network;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface NetworkReporter {
|
||||||
|
void reportRequest(RequestInfo requestInfo);
|
||||||
|
|
||||||
|
void reportResponse(ResponseInfo responseInfo);
|
||||||
|
|
||||||
|
public class Header {
|
||||||
|
public final String name;
|
||||||
|
public final String value;
|
||||||
|
|
||||||
|
public Header(final String name, final String value) {
|
||||||
|
this.name = name;
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "Header{" + name + ": " + value + "}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class RequestInfo {
|
||||||
|
public String requestId;
|
||||||
|
public long timeStamp;
|
||||||
|
public List<Header> headers = new ArrayList<>();
|
||||||
|
public String method;
|
||||||
|
public String uri;
|
||||||
|
public byte[] body;
|
||||||
|
|
||||||
|
public Header getFirstHeader(final String name) {
|
||||||
|
for (Header header : headers) {
|
||||||
|
if (name.equalsIgnoreCase(header.name)) {
|
||||||
|
return header;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ResponseInfo {
|
||||||
|
public String requestId;
|
||||||
|
public long timeStamp;
|
||||||
|
public int statusCode;
|
||||||
|
public String statusReason;
|
||||||
|
public List<Header> headers = new ArrayList<>();
|
||||||
|
public byte[] body;
|
||||||
|
|
||||||
|
public Header getFirstHeader(final String name) {
|
||||||
|
for (Header header : headers) {
|
||||||
|
if (name.equalsIgnoreCase(header.name)) {
|
||||||
|
return header;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
22
android/plugins/network/NetworkResponseFormatter.java
Normal file
22
android/plugins/network/NetworkResponseFormatter.java
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* 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.sonar.plugins.network;
|
||||||
|
|
||||||
|
import com.facebook.sonar.plugins.network.NetworkReporter.ResponseInfo;
|
||||||
|
|
||||||
|
public interface NetworkResponseFormatter {
|
||||||
|
|
||||||
|
interface OnCompletionListener {
|
||||||
|
void onCompletion(String json);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean shouldFormat(ResponseInfo response);
|
||||||
|
|
||||||
|
void format(ResponseInfo response, OnCompletionListener onCompletionListener);
|
||||||
|
}
|
||||||
122
android/plugins/network/NetworkSonarPlugin.java
Normal file
122
android/plugins/network/NetworkSonarPlugin.java
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* 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.sonar.plugins.network;
|
||||||
|
|
||||||
|
import android.util.Base64;
|
||||||
|
import com.facebook.sonar.core.ErrorReportingRunnable;
|
||||||
|
import com.facebook.sonar.core.SonarArray;
|
||||||
|
import com.facebook.sonar.core.SonarObject;
|
||||||
|
import com.facebook.sonar.plugins.common.BufferingSonarPlugin;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class NetworkSonarPlugin extends BufferingSonarPlugin implements NetworkReporter {
|
||||||
|
public static final String ID = "Network";
|
||||||
|
|
||||||
|
private final List<NetworkResponseFormatter> mFormatters;
|
||||||
|
|
||||||
|
public NetworkSonarPlugin() {
|
||||||
|
this(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public NetworkSonarPlugin(List<NetworkResponseFormatter> formatters) {
|
||||||
|
this.mFormatters = formatters;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reportRequest(RequestInfo requestInfo) {
|
||||||
|
final SonarObject request =
|
||||||
|
new SonarObject.Builder()
|
||||||
|
.put("id", requestInfo.requestId)
|
||||||
|
.put("timestamp", requestInfo.timeStamp)
|
||||||
|
.put("method", requestInfo.method)
|
||||||
|
.put("url", requestInfo.uri)
|
||||||
|
.put("headers", toSonarObject(requestInfo.headers))
|
||||||
|
.put("data", toBase64(requestInfo.body))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
send("newRequest", request);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reportResponse(final ResponseInfo responseInfo) {
|
||||||
|
final Runnable job =
|
||||||
|
new ErrorReportingRunnable(getConnection()) {
|
||||||
|
@Override
|
||||||
|
protected void runOrThrow() throws Exception {
|
||||||
|
if (shouldStripResponseBody(responseInfo)) {
|
||||||
|
responseInfo.body = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final SonarObject response =
|
||||||
|
new SonarObject.Builder()
|
||||||
|
.put("id", responseInfo.requestId)
|
||||||
|
.put("timestamp", responseInfo.timeStamp)
|
||||||
|
.put("status", responseInfo.statusCode)
|
||||||
|
.put("reason", responseInfo.statusReason)
|
||||||
|
.put("headers", toSonarObject(responseInfo.headers))
|
||||||
|
.put("data", toBase64(responseInfo.body))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
send("newResponse", response);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (mFormatters != null) {
|
||||||
|
for (NetworkResponseFormatter formatter : mFormatters) {
|
||||||
|
if (formatter.shouldFormat(responseInfo)) {
|
||||||
|
formatter.format(
|
||||||
|
responseInfo,
|
||||||
|
new NetworkResponseFormatter.OnCompletionListener() {
|
||||||
|
@Override
|
||||||
|
public void onCompletion(final String json) {
|
||||||
|
responseInfo.body = json.getBytes();
|
||||||
|
job.run();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
job.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String toBase64(byte[] bytes) {
|
||||||
|
if (bytes == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new String(Base64.encode(bytes, Base64.DEFAULT));
|
||||||
|
}
|
||||||
|
|
||||||
|
private SonarArray toSonarObject(List<Header> headers) {
|
||||||
|
final SonarArray.Builder list = new SonarArray.Builder();
|
||||||
|
|
||||||
|
for (Header header : headers) {
|
||||||
|
list.put(new SonarObject.Builder().put("key", header.name).put("value", header.value));
|
||||||
|
}
|
||||||
|
|
||||||
|
return list.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean shouldStripResponseBody(ResponseInfo responseInfo) {
|
||||||
|
final Header contentType = responseInfo.getFirstHeader("content-type");
|
||||||
|
if (contentType == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return contentType.value.contains("image/")
|
||||||
|
|| contentType.value.contains("video/")
|
||||||
|
|| contentType.value.contains("application/zip");
|
||||||
|
}
|
||||||
|
}
|
||||||
106
android/plugins/network/SonarOkhttpInterceptor.java
Normal file
106
android/plugins/network/SonarOkhttpInterceptor.java
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
// Copyright 2004-present Facebook. All Rights Reserved.
|
||||||
|
package com.facebook.sonar.plugins.network;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
import com.facebook.sonar.plugins.network.NetworkReporter.RequestInfo;
|
||||||
|
import com.facebook.sonar.plugins.network.NetworkReporter.ResponseInfo;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Random;
|
||||||
|
import java.util.Set;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import okhttp3.Headers;
|
||||||
|
import okhttp3.Interceptor;
|
||||||
|
import okhttp3.Request;
|
||||||
|
import okhttp3.Response;
|
||||||
|
import okhttp3.ResponseBody;
|
||||||
|
import okio.Buffer;
|
||||||
|
|
||||||
|
public class SonarOkhttpInterceptor implements Interceptor {
|
||||||
|
|
||||||
|
public @Nullable NetworkSonarPlugin plugin;
|
||||||
|
|
||||||
|
public SonarOkhttpInterceptor() {
|
||||||
|
this.plugin = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SonarOkhttpInterceptor(NetworkSonarPlugin plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Response intercept(Interceptor.Chain chain) throws IOException {
|
||||||
|
Request request = chain.request();
|
||||||
|
int randInt = randInt(1, Integer.MAX_VALUE);
|
||||||
|
plugin.reportRequest(convertRequest(request, randInt));
|
||||||
|
Response response = chain.proceed(request);
|
||||||
|
ResponseBody body = response.body();
|
||||||
|
ResponseInfo responseInfo = convertResponse(response, body, randInt);
|
||||||
|
plugin.reportResponse(responseInfo);
|
||||||
|
// Creating new response as can't used response.body() more than once
|
||||||
|
return response
|
||||||
|
.newBuilder()
|
||||||
|
.body(ResponseBody.create(body.contentType(), responseInfo.body))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] bodyToByteArray(final Request request) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
final Request copy = request.newBuilder().build();
|
||||||
|
final Buffer buffer = new Buffer();
|
||||||
|
copy.body().writeTo(buffer);
|
||||||
|
return buffer.readByteArray();
|
||||||
|
} catch (final IOException e) {
|
||||||
|
return e.getMessage().getBytes();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private RequestInfo convertRequest(Request request, int identifier) {
|
||||||
|
List<NetworkReporter.Header> headers = convertHeader(request.headers());
|
||||||
|
RequestInfo info = new RequestInfo();
|
||||||
|
info.requestId = String.valueOf(identifier);
|
||||||
|
info.timeStamp = System.currentTimeMillis();
|
||||||
|
info.headers = headers;
|
||||||
|
info.method = request.method();
|
||||||
|
info.uri = request.url().toString();
|
||||||
|
if (request.body() != null) {
|
||||||
|
info.body = bodyToByteArray(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ResponseInfo convertResponse(Response response, ResponseBody body, int identifier) {
|
||||||
|
|
||||||
|
List<NetworkReporter.Header> headers = convertHeader(response.headers());
|
||||||
|
ResponseInfo info = new ResponseInfo();
|
||||||
|
info.requestId = String.valueOf(identifier);
|
||||||
|
info.timeStamp = response.receivedResponseAtMillis();
|
||||||
|
info.statusCode = response.code();
|
||||||
|
info.headers = headers;
|
||||||
|
try {
|
||||||
|
info.body = body.bytes();
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.e("Sonar", e.toString());
|
||||||
|
}
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<NetworkReporter.Header> convertHeader(Headers headers) {
|
||||||
|
List<NetworkReporter.Header> list = new ArrayList<>();
|
||||||
|
|
||||||
|
Set<String> keys = headers.names();
|
||||||
|
for (String key : keys) {
|
||||||
|
list.add(new NetworkReporter.Header(key, headers.get(key)));
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int randInt(int min, int max) {
|
||||||
|
Random rand = new Random();
|
||||||
|
int randomNum = rand.nextInt((max - min) + 1) + min;
|
||||||
|
return randomNum;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* 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.sonar.plugins.sharedpreferences;
|
||||||
|
|
||||||
|
import static android.content.Context.MODE_PRIVATE;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import com.facebook.sonar.core.SonarConnection;
|
||||||
|
import com.facebook.sonar.core.SonarObject;
|
||||||
|
import com.facebook.sonar.core.SonarPlugin;
|
||||||
|
import com.facebook.sonar.core.SonarReceiver;
|
||||||
|
import com.facebook.sonar.core.SonarResponder;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class SharedPreferencesSonarPlugin implements SonarPlugin {
|
||||||
|
|
||||||
|
private SonarConnection mConnection;
|
||||||
|
private final SharedPreferences mSharedPreferences;
|
||||||
|
|
||||||
|
public SharedPreferencesSonarPlugin(Context context) {
|
||||||
|
mSharedPreferences = context.getSharedPreferences(context.getPackageName(), MODE_PRIVATE);
|
||||||
|
|
||||||
|
mSharedPreferences.registerOnSharedPreferenceChangeListener(
|
||||||
|
new SharedPreferences.OnSharedPreferenceChangeListener() {
|
||||||
|
@Override
|
||||||
|
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
|
||||||
|
if (mConnection != null) {
|
||||||
|
mConnection.send(
|
||||||
|
"sharedPreferencesChange",
|
||||||
|
new SonarObject.Builder()
|
||||||
|
.put("name", key)
|
||||||
|
.put("deleted", !mSharedPreferences.contains(key))
|
||||||
|
.put("time", System.currentTimeMillis())
|
||||||
|
.put("value", mSharedPreferences.getAll().get(key))
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return "Preferences";
|
||||||
|
}
|
||||||
|
|
||||||
|
private SonarObject getSharedPreferencesObject() {
|
||||||
|
final SonarObject.Builder builder = new SonarObject.Builder();
|
||||||
|
final Map<String, ?> map = mSharedPreferences.getAll();
|
||||||
|
|
||||||
|
for (Map.Entry<String, ?> entry : map.entrySet()) {
|
||||||
|
final Object val = entry.getValue();
|
||||||
|
builder.put(entry.getKey(), val);
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onConnect(SonarConnection connection) {
|
||||||
|
mConnection = connection;
|
||||||
|
|
||||||
|
connection.receive(
|
||||||
|
"getSharedPreferences",
|
||||||
|
new SonarReceiver() {
|
||||||
|
@Override
|
||||||
|
public void onReceive(SonarObject params, SonarResponder responder) {
|
||||||
|
responder.success(getSharedPreferencesObject());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
connection.receive(
|
||||||
|
"setSharedPreference",
|
||||||
|
new SonarReceiver() {
|
||||||
|
@Override
|
||||||
|
public void onReceive(SonarObject params, SonarResponder responder)
|
||||||
|
throws IllegalArgumentException {
|
||||||
|
|
||||||
|
String preferenceName = params.getString("preferenceName");
|
||||||
|
Object originalValue = mSharedPreferences.getAll().get(preferenceName);
|
||||||
|
SharedPreferences.Editor editor = mSharedPreferences.edit();
|
||||||
|
|
||||||
|
if (originalValue instanceof Boolean) {
|
||||||
|
editor.putBoolean(preferenceName, params.getBoolean("preferenceValue"));
|
||||||
|
} else if (originalValue instanceof Long) {
|
||||||
|
editor.putLong(preferenceName, params.getLong("preferenceValue"));
|
||||||
|
} else if (originalValue instanceof Integer) {
|
||||||
|
editor.putInt(preferenceName, params.getInt("preferenceValue"));
|
||||||
|
} else if (originalValue instanceof Float) {
|
||||||
|
editor.putFloat(preferenceName, params.getFloat("preferenceValue"));
|
||||||
|
} else if (originalValue instanceof String) {
|
||||||
|
editor.putString(preferenceName, params.getString("preferenceValue"));
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("Type not supported: " + preferenceName);
|
||||||
|
}
|
||||||
|
|
||||||
|
editor.apply();
|
||||||
|
|
||||||
|
responder.success(getSharedPreferencesObject());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDisconnect() {
|
||||||
|
mConnection = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
29
android/sample/AndroidManifest.xml
Normal file
29
android/sample/AndroidManifest.xml
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="com.facebook.sonar.sample">
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||||
|
|
||||||
|
<uses-sdk
|
||||||
|
android:minSdkVersion="15"
|
||||||
|
android:targetSdkVersion="24"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:name=".SonarSampleApplication"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:icon="@drawable/ic_launcher"
|
||||||
|
android:allowBackup="false"
|
||||||
|
android:theme="@style/NoTitleBarWhiteBG">
|
||||||
|
<activity android:name=".MainActivity">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
</application>
|
||||||
|
|
||||||
|
|
||||||
|
</manifest>
|
||||||
63
android/sample/build.gradle
Normal file
63
android/sample/build.gradle
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
apply plugin: 'com.android.application'
|
||||||
|
|
||||||
|
android {
|
||||||
|
|
||||||
|
compileSdkVersion rootProject.compileSdkVersion
|
||||||
|
buildToolsVersion rootProject.buildToolsVersion
|
||||||
|
defaultConfig {
|
||||||
|
minSdkVersion rootProject.minSdkVersion
|
||||||
|
testInstrumentationRunner 'android.support.test.runner.AndroidJUnitRunner'
|
||||||
|
applicationId "com.facebook.sonar.sample"
|
||||||
|
}
|
||||||
|
|
||||||
|
lintOptions {
|
||||||
|
abortOnError false
|
||||||
|
}
|
||||||
|
sourceSets {
|
||||||
|
main {
|
||||||
|
manifest.srcFile './AndroidManifest.xml'
|
||||||
|
java {
|
||||||
|
srcDir 'src'
|
||||||
|
}
|
||||||
|
res {
|
||||||
|
srcDir 'res'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
packagingOptions {
|
||||||
|
pickFirst 'lib/armeabi-v7a/libfb.so'
|
||||||
|
pickFirst 'lib/x86/libfb.so'
|
||||||
|
pickFirst 'lib/x86_64/libfb.so'
|
||||||
|
pickFirst 'lib/arm64-v8a/libfb.so'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||||
|
implementation 'com.android.support:appcompat-v7:26.1.0'
|
||||||
|
implementation 'com.android.support.constraint:constraint-layout:1.1.0'
|
||||||
|
implementation 'com.android.support:design:26.1.0'
|
||||||
|
testImplementation 'junit:junit:4.12'
|
||||||
|
androidTestImplementation 'com.android.support.test:runner:1.0.2'
|
||||||
|
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
|
||||||
|
// ...
|
||||||
|
// Litho
|
||||||
|
implementation 'com.facebook.litho:litho-core:0.15.0'
|
||||||
|
implementation 'com.facebook.litho:litho-widget:0.15.0'
|
||||||
|
compileOnly 'com.facebook.litho:litho-annotations:0.15.0'
|
||||||
|
|
||||||
|
annotationProcessor 'com.facebook.litho:litho-processor:0.15.0'
|
||||||
|
|
||||||
|
// SoLoader
|
||||||
|
implementation 'com.facebook.soloader:soloader:0.4.1'
|
||||||
|
|
||||||
|
// For integration with Fresco
|
||||||
|
implementation 'com.facebook.litho:litho-fresco:0.15.0'
|
||||||
|
|
||||||
|
// For testing
|
||||||
|
testImplementation 'com.facebook.litho:litho-testing:0.15.0'
|
||||||
|
|
||||||
|
implementation 'com.squareup.okhttp3:okhttp:3.10.0'
|
||||||
|
implementation project(':android')
|
||||||
|
//implementation project(':sonar')
|
||||||
|
}
|
||||||
BIN
android/sample/debug.keystore
Normal file
BIN
android/sample/debug.keystore
Normal file
Binary file not shown.
3
android/sample/debug.keystore.properties
Normal file
3
android/sample/debug.keystore.properties
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
key.alias=androiddebugkey
|
||||||
|
key.store.password=android
|
||||||
|
key.alias.password=android
|
||||||
BIN
android/sample/res/drawable-hdpi/ic_launcher.png
Normal file
BIN
android/sample/res/drawable-hdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.8 KiB |
BIN
android/sample/res/drawable-mdpi/ic_launcher.png
Normal file
BIN
android/sample/res/drawable-mdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
BIN
android/sample/res/drawable-xhdpi/ic_launcher.png
Normal file
BIN
android/sample/res/drawable-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.4 KiB |
BIN
android/sample/res/drawable-xxhdpi/ic_launcher.png
Normal file
BIN
android/sample/res/drawable-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.7 KiB |
BIN
android/sample/res/drawable-xxxhdpi/ic_launcher.png
Normal file
BIN
android/sample/res/drawable-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.8 KiB |
5
android/sample/res/values/strings.xml
Normal file
5
android/sample/res/values/strings.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<resources>
|
||||||
|
<string name="app_name">Sonar</string>
|
||||||
|
</resources>
|
||||||
7
android/sample/res/values/styles.xml
Normal file
7
android/sample/res/values/styles.xml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<resources>
|
||||||
|
<style name="NoTitleBarWhiteBG" parent="Theme.AppCompat.Light">
|
||||||
|
<item name="android:textColor">#000000</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
// Copyright 2004-present Facebook. All Rights Reserved.
|
||||||
|
|
||||||
|
package com.facebook.sonar.sample;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.support.v7.app.AppCompatActivity;
|
||||||
|
import com.facebook.litho.ComponentContext;
|
||||||
|
import com.facebook.litho.LithoView;
|
||||||
|
|
||||||
|
public class MainActivity extends AppCompatActivity {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
final ComponentContext c = new ComponentContext(this);
|
||||||
|
setContentView(
|
||||||
|
LithoView.create(
|
||||||
|
c,
|
||||||
|
RootComponent.create(c).build()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
// Copyright 2004-present Facebook. All Rights Reserved.
|
||||||
|
|
||||||
|
package com.facebook.sonar.sample;
|
||||||
|
|
||||||
|
import com.facebook.litho.Column;
|
||||||
|
import com.facebook.litho.Component;
|
||||||
|
import com.facebook.litho.ComponentContext;
|
||||||
|
import com.facebook.litho.annotations.LayoutSpec;
|
||||||
|
import com.facebook.litho.annotations.OnCreateLayout;
|
||||||
|
import com.facebook.litho.widget.Text;
|
||||||
|
import okhttp3.Request;
|
||||||
|
import okhttp3.Response;
|
||||||
|
import okhttp3.RequestBody;
|
||||||
|
import okhttp3.FormBody;
|
||||||
|
import okhttp3.FormBody.Builder;
|
||||||
|
import okhttp3.OkHttpClient;
|
||||||
|
import okhttp3.Call;
|
||||||
|
import okhttp3.Callback;
|
||||||
|
import com.facebook.litho.ClickEvent;
|
||||||
|
import android.util.Log;
|
||||||
|
import java.io.IOException;
|
||||||
|
import com.facebook.litho.annotations.OnEvent;
|
||||||
|
|
||||||
|
@LayoutSpec
|
||||||
|
public class RootComponentSpec {
|
||||||
|
|
||||||
|
@OnCreateLayout
|
||||||
|
static Component onCreateLayout(ComponentContext c) {
|
||||||
|
return Column.create(c)
|
||||||
|
.child(
|
||||||
|
Text.create(c)
|
||||||
|
.text("Tap to hit get request")
|
||||||
|
.key("1")
|
||||||
|
.textSizeSp(20)
|
||||||
|
.clickHandler(RootComponent.hitGetRequest(c)))
|
||||||
|
.child(
|
||||||
|
Text.create(c)
|
||||||
|
.text("Tap to hit post request")
|
||||||
|
.key("2")
|
||||||
|
.textSizeSp(20)
|
||||||
|
.clickHandler(RootComponent.hitPostRequest(c)))
|
||||||
|
.child(
|
||||||
|
Text.create(c)
|
||||||
|
.text("I m just a text")
|
||||||
|
.key("3")
|
||||||
|
.textSizeSp(20))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnEvent(ClickEvent.class)
|
||||||
|
static void hitGetRequest(ComponentContext c) {
|
||||||
|
|
||||||
|
Request request = new Request.Builder()
|
||||||
|
.url("https://api.github.com/repos/facebook/yoga")
|
||||||
|
.get()
|
||||||
|
.build();
|
||||||
|
SonarSampleApplication.okhttpClient.newCall(request).enqueue(new Callback() {
|
||||||
|
@Override
|
||||||
|
public void onFailure(Call call, IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
Log.d("Sonar", e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onResponse(Call call, Response response) throws IOException {
|
||||||
|
if (response.isSuccessful()) {
|
||||||
|
Log.d("Sonar", response.body().string());
|
||||||
|
} else {
|
||||||
|
Log.d("Sonar", "not successful");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnEvent(ClickEvent.class)
|
||||||
|
static void hitPostRequest(ComponentContext c) {
|
||||||
|
|
||||||
|
RequestBody formBody = new FormBody.Builder()
|
||||||
|
.add("app", "Sonar")
|
||||||
|
.add("remarks", "Its awesome")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Request request = new Request.Builder()
|
||||||
|
.url("https://demo9512366.mockable.io/SonarPost")
|
||||||
|
.post(formBody)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
SonarSampleApplication.okhttpClient.newCall(request).enqueue(new Callback() {
|
||||||
|
@Override
|
||||||
|
public void onFailure(Call call, IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
Log.d("Sonar", e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onResponse(Call call, Response response) throws IOException {
|
||||||
|
if (response.isSuccessful()) {
|
||||||
|
Log.d("Sonar", response.body().string());
|
||||||
|
} else {
|
||||||
|
Log.d("Sonar", "not successful");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
// Copyright 2004-present Facebook. All Rights Reserved.
|
||||||
|
|
||||||
|
package com.facebook.sonar.sample;
|
||||||
|
|
||||||
|
import android.app.Application;
|
||||||
|
import android.net.Network;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import com.facebook.litho.sonar.LithoSonarDescriptors;
|
||||||
|
import com.facebook.soloader.SoLoader;
|
||||||
|
import com.facebook.sonar.android.utils.SonarUtils;
|
||||||
|
import com.facebook.sonar.android.AndroidSonarClient;
|
||||||
|
import com.facebook.sonar.core.SonarClient;
|
||||||
|
import com.facebook.sonar.plugins.inspector.DescriptorMapping;
|
||||||
|
import com.facebook.sonar.plugins.inspector.InspectorSonarPlugin;
|
||||||
|
import com.facebook.sonar.plugins.network.NetworkSonarPlugin;
|
||||||
|
import com.facebook.sonar.plugins.network.SonarOkhttpInterceptor;
|
||||||
|
import com.facebook.sonar.plugins.network.NetworkResponseFormatter;
|
||||||
|
import okhttp3.OkHttpClient;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
public class SonarSampleApplication extends Application {
|
||||||
|
|
||||||
|
static public OkHttpClient okhttpClient;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate() {
|
||||||
|
super.onCreate();
|
||||||
|
SoLoader.init(this, false);
|
||||||
|
|
||||||
|
final SonarClient client = AndroidSonarClient.getInstance(this);
|
||||||
|
final DescriptorMapping descriptorMapping = DescriptorMapping.withDefaults();
|
||||||
|
|
||||||
|
NetworkSonarPlugin networkPlugin = new NetworkSonarPlugin();
|
||||||
|
SonarOkhttpInterceptor interceptor = new SonarOkhttpInterceptor(networkPlugin);
|
||||||
|
|
||||||
|
okhttpClient = new OkHttpClient.Builder()
|
||||||
|
.addNetworkInterceptor(interceptor)
|
||||||
|
.connectTimeout(60, TimeUnit.SECONDS)
|
||||||
|
.readTimeout(60, TimeUnit.SECONDS)
|
||||||
|
.writeTimeout(10, TimeUnit.MINUTES)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
LithoSonarDescriptors.add(descriptorMapping);
|
||||||
|
client.addPlugin(new InspectorSonarPlugin(this, descriptorMapping));
|
||||||
|
client.addPlugin(networkPlugin);
|
||||||
|
client.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
56
android/testing/SonarConnectionMock.java
Normal file
56
android/testing/SonarConnectionMock.java
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* 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.sonar.testing;
|
||||||
|
|
||||||
|
import com.facebook.sonar.core.SonarArray;
|
||||||
|
import com.facebook.sonar.core.SonarConnection;
|
||||||
|
import com.facebook.sonar.core.SonarObject;
|
||||||
|
import com.facebook.sonar.core.SonarReceiver;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class SonarConnectionMock implements SonarConnection {
|
||||||
|
public final Map<String, SonarReceiver> receivers = new HashMap<>();
|
||||||
|
public final Map<String, List<Object>> sent = new HashMap<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void send(String method, SonarObject params) {
|
||||||
|
final List<Object> paramList;
|
||||||
|
if (sent.containsKey(method)) {
|
||||||
|
paramList = sent.get(method);
|
||||||
|
} else {
|
||||||
|
paramList = new ArrayList<>();
|
||||||
|
sent.put(method, paramList);
|
||||||
|
}
|
||||||
|
|
||||||
|
paramList.add(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void send(String method, SonarArray params) {
|
||||||
|
final List<Object> paramList;
|
||||||
|
if (sent.containsKey(method)) {
|
||||||
|
paramList = sent.get(method);
|
||||||
|
} else {
|
||||||
|
paramList = new ArrayList<>();
|
||||||
|
sent.put(method, paramList);
|
||||||
|
}
|
||||||
|
|
||||||
|
paramList.add(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reportError(Throwable throwable) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void receive(String method, SonarReceiver receiver) {
|
||||||
|
receivers.put(method, receiver);
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user