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:
Daniel Büchele
2018-04-13 08:38:06 -07:00
committed by Daniel Buchele
commit fbbf8cf16b
659 changed files with 87130 additions and 0 deletions

View 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
View 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)

View 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();
}
}

View 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();
}

View 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();
}

View 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);
}

View 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);
}

View 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
View 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();
});
}

View 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
View 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
}

View 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;
}

View 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);
}
}
}

View 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();
}

View 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);
}

View 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;
}
}

View 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);
}
}
}

View 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;
}

View 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;
}

View 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);
}

View 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();
}

View 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;
}
}
}

View 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;
}

View 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;
}
}

View 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 {
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
};
}

View 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;
}
}

View 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();
}

View 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();
}

View 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);
}
}

View 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);
}
}

View 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);
}
}
}

View 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 {}

View 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);
}
}

View 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;
}
}

View 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();
}
}

View 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;
}
}

View 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;
}

View 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);
}
}

View 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();
}
}

View 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);
}

View 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);
}

View 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;
}
}

View 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);
}
}

View 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);
}
}

View File

@@ -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);
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View 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);
}
}

View 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);
}
};
}

View 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;
}
}
}

View 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);
}
}

View File

@@ -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();
}
}
}

View File

@@ -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;
}
}

View File

@@ -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));
}
}
}

View File

@@ -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) {
}
}
}

View 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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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());
}
}

View 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);
}
}

View File

@@ -0,0 +1,8 @@
// Copyright 2004-present Facebook. All Rights Reserved.
package com.facebook.litho.sonar;
public interface PropWithDescription {
Object getSonarLayoutInspectorPropDescription();
}

View 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;
}
}
}

View 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);
}

View 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");
}
}

View 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;
}
}

View File

@@ -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;
}
}

View 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>

View 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')
}

Binary file not shown.

View File

@@ -0,0 +1,3 @@
key.alias=androiddebugkey
key.store.password=android
key.alias.password=android

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Sonar</string>
</resources>

View 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>

View File

@@ -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()));
}
}

View File

@@ -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");
}
}
});
}
}

View File

@@ -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();
}
}

View 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);
}
}

View File

@@ -0,0 +1,39 @@
/*
* 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.SonarObject;
import com.facebook.sonar.core.SonarResponder;
import java.util.LinkedList;
import java.util.List;
public class SonarResponderMock implements SonarResponder {
public final List<Object> successes = new LinkedList<>();
public final List<SonarObject> errors = new LinkedList<>();
@Override
public void success(SonarObject response) {
successes.add(response);
}
@Override
public void success(SonarArray response) {
successes.add(response);
}
@Override
public void success() {
successes.add(new SonarObject.Builder().build());
}
@Override
public void error(SonarObject response) {
errors.add(response);
}
}

View File

@@ -0,0 +1,49 @@
/*
* 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 static org.hamcrest.CoreMatchers.hasItem;
import static org.hamcrest.MatcherAssert.assertThat;
import com.facebook.sonar.core.SonarObject;
import com.facebook.sonar.testing.SonarConnectionMock;
import com.facebook.sonar.testing.SonarResponderMock;
import com.facebook.testing.robolectric.v3.WithTestDefaultsRunner;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@RunWith(WithTestDefaultsRunner.class)
public class ConsoleSonarPluginTest {
SonarConnectionMock connection;
SonarResponderMock responder;
@Before
public void setup() throws Exception {
JavascriptEnvironment jsEnvironment = new JavascriptEnvironment();
final ConsoleSonarPlugin plugin = new ConsoleSonarPlugin(jsEnvironment);
connection = new SonarConnectionMock();
responder = new SonarResponderMock();
plugin.onConnect(connection);
}
@Test
public void simpleExpressionShouldEvaluateCorrectly() throws Exception {
receiveScript("2 + 2");
assertThat(
responder.successes,
hasItem(new SonarObject.Builder().put("value", 4).put("type", "json").build()));
}
private void receiveScript(String a) throws Exception {
SonarObject getValue = new SonarObject.Builder().put("command", a).build();
connection.receivers.get("executeCommand").onReceive(getValue, responder);
}
}

View File

@@ -0,0 +1,109 @@
/*
* 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 static org.junit.Assert.assertEquals;
import com.facebook.testing.robolectric.v3.WithTestDefaultsRunner;
import com.google.common.collect.ImmutableMap;
import java.util.Collections;
import java.util.HashMap;
import org.json.JSONObject;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mozilla.javascript.ContextFactory;
@RunWith(WithTestDefaultsRunner.class)
public class JavascriptSessionTest {
ContextFactory mContextFactory = new ContextFactory();
@Test
public void testSimpleExpressionsEvaluate() throws Exception {
JavascriptSession session =
new JavascriptSession(mContextFactory, Collections.<String, Object>emptyMap());
JSONObject json = session.evaluateCommand("2+2-1");
assertEquals(3, json.getInt("value"));
}
@Test
public void testStatePersistsBetweenCommands() throws Exception {
JavascriptSession session =
new JavascriptSession(mContextFactory, Collections.<String, Object>emptyMap());
session.evaluateCommand("var x = 10;");
JSONObject json = session.evaluateCommand("x");
assertEquals(10, json.getInt("value"));
}
@Test
public void testVariablesGetBoundCorrectly() throws Exception {
JavascriptSession session =
new JavascriptSession(
mContextFactory,
ImmutableMap.<String, Object>of(
"a", 2,
"b", 2));
JSONObject json = session.evaluateCommand("a+b");
assertEquals("json", json.getString("type"));
assertEquals(4, json.getInt("value"));
}
@Test
public void testNumberEvaluation() throws Exception {
assertEquals(4, evaluateWithNoGlobals("4").getInt("value"));
}
@Test
public void testStringEvaluation() throws Exception {
assertEquals("hello", evaluateWithNoGlobals("\"hello\"").getString("value"));
}
@Test
public void testJavaObjectEvaluation() throws Exception {
JavascriptSession session =
new JavascriptSession(
mContextFactory,
ImmutableMap.<String, Object>of("object", new HashMap<String, String>()));
JSONObject json = session.evaluateCommand("object");
assertEquals("javaObject", json.getString("type"));
assertEquals("{}", json.getJSONObject("value").getString("toString"));
}
@Test
public void testJavaMethodEvaluation() throws Exception {
JavascriptSession session =
new JavascriptSession(
mContextFactory,
ImmutableMap.<String, Object>of("object", new HashMap<String, String>()));
JSONObject json = session.evaluateCommand("object.get");
assertEquals("method", json.getString("type"));
}
@Test
public void testJsFunctionEvaluation() throws Exception {
JSONObject json = evaluateWithNoGlobals("function() {}");
assertEquals("function", json.getString("type"));
assertEquals("function(){}", removeWhitespace(json.getString("value")));
}
@Test
public void testNullEvaluation() throws Exception {
assertEquals("null", evaluateWithNoGlobals("null").getString("type"));
assertEquals("null", evaluateWithNoGlobals("undefined").getString("type"));
}
private static String removeWhitespace(String input) {
return input.replaceAll("\\s", "");
}
private JSONObject evaluateWithNoGlobals(String input) throws Exception {
JavascriptSession session =
new JavascriptSession(mContextFactory, new HashMap<String, Object>());
return session.evaluateCommand(input);
}
}

View File

@@ -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;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.MatcherAssert.assertThat;
import android.app.Activity;
import android.app.Application;
import android.app.Application.ActivityLifecycleCallbacks;
import android.os.Bundle;
import com.facebook.testing.robolectric.v3.WithTestDefaultsRunner;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
@RunWith(WithTestDefaultsRunner.class)
public class ApplicationWrapperTest {
private ApplicationWrapper mWrapper;
private ActivityLifecycleCallbacks mCallbacks;
@Before
public void setup() {
final Application app = Mockito.mock(Application.class);
Mockito.doAnswer(
new Answer() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
mCallbacks = (ActivityLifecycleCallbacks) invocation.getArguments()[0];
return null;
}
})
.when(app)
.registerActivityLifecycleCallbacks(Mockito.any(ActivityLifecycleCallbacks.class));
mWrapper = new ApplicationWrapper(app);
}
@Test
public void testActivityCreated() {
final Activity activity1 = Mockito.mock(Activity.class);
mCallbacks.onActivityCreated(activity1, Mockito.mock(Bundle.class));
final Activity activity2 = Mockito.mock(Activity.class);
mCallbacks.onActivityCreated(activity2, Mockito.mock(Bundle.class));
assertThat(mWrapper.getActivityStack().size(), equalTo(2));
assertThat(mWrapper.getActivityStack().get(0), equalTo(activity1));
assertThat(mWrapper.getActivityStack().get(1), equalTo(activity2));
}
@Test
public void testActivityPaused() {
final Activity activity1 = Mockito.mock(Activity.class);
mCallbacks.onActivityCreated(activity1, Mockito.mock(Bundle.class));
final Activity activity2 = Mockito.mock(Activity.class);
mCallbacks.onActivityCreated(activity2, Mockito.mock(Bundle.class));
mCallbacks.onActivityPaused(activity2);
assertThat(mWrapper.getActivityStack().size(), equalTo(2));
assertThat(mWrapper.getActivityStack().get(0), equalTo(activity1));
assertThat(mWrapper.getActivityStack().get(1), equalTo(activity2));
}
@Test
public void testFinishingActivityPaused() {
final Activity activity1 = Mockito.mock(Activity.class);
mCallbacks.onActivityCreated(activity1, Mockito.mock(Bundle.class));
final Activity activity2 = Mockito.mock(Activity.class);
mCallbacks.onActivityCreated(activity2, Mockito.mock(Bundle.class));
Mockito.when(activity2.isFinishing()).thenReturn(true);
mCallbacks.onActivityPaused(activity2);
assertThat(mWrapper.getActivityStack().size(), equalTo(1));
assertThat(mWrapper.getActivityStack().get(0), equalTo(activity1));
}
}

View 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;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.MatcherAssert.assertThat;
import com.facebook.sonar.core.SonarConnection;
import com.facebook.sonar.core.SonarDynamic;
import com.facebook.sonar.core.SonarObject;
import com.facebook.sonar.testing.SonarConnectionMock;
import com.facebook.testing.robolectric.v3.WithTestDefaultsRunner;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
@RunWith(WithTestDefaultsRunner.class)
public class DescriptorMappingTest {
private class TestClass {}
private class TestSubClass extends TestClass {}
private class TestDescriptor<T> extends NodeDescriptor<T> {
@Override
public void init(T node) {}
@Override
public String getId(T node) {
return null;
}
@Override
public String getName(T node) {
return null;
}
@Override
public int getChildCount(T node) {
return 0;
}
@Override
public T getChildAt(T node, int index) {
return null;
}
@Override
public List<Named<SonarObject>> getData(T node) {
return null;
}
@Override
public void setValue(T node, String[] path, SonarDynamic value) throws Exception {}
@Override
public List<Named<String>> getAttributes(T node) {
return null;
}
@Override
public void setHighlighted(T node, boolean selected) {}
@Override
public void hitTest(T node, Touch touch) {}
@Override
public String getDecoration(T obj) {
return null;
}
@Override
public boolean matches(String query, T obj) {
return false;
}
}
@Test
public void testDescriptorForRegisteredClass() {
final DescriptorMapping descriptorMapping = new DescriptorMapping();
final NodeDescriptor descriptor1 = new TestDescriptor<>();
final NodeDescriptor descriptor2 = new TestDescriptor<>();
descriptorMapping.register(TestClass.class, descriptor1);
descriptorMapping.register(TestSubClass.class, descriptor2);
assertThat(descriptorMapping.descriptorForClass(TestSubClass.class), equalTo(descriptor2));
}
@Test
public void testDescriptorForRegisteredSuperClass() {
final DescriptorMapping descriptorMapping = new DescriptorMapping();
final NodeDescriptor descriptor = new TestDescriptor<>();
descriptorMapping.register(TestClass.class, descriptor);
assertThat(descriptorMapping.descriptorForClass(TestSubClass.class), equalTo(descriptor));
}
@Test
public void testOnConnect() {
final DescriptorMapping descriptorMapping = new DescriptorMapping();
final NodeDescriptor descriptor = new TestDescriptor<>();
descriptorMapping.register(TestClass.class, descriptor);
final SonarConnection connection = new SonarConnectionMock();
descriptorMapping.onConnect(connection);
assertThat(descriptor.connected(), equalTo(true));
}
@Test
public void testOnDisconnect() {
final DescriptorMapping descriptorMapping = new DescriptorMapping();
final NodeDescriptor descriptor = new TestDescriptor<>();
descriptorMapping.register(TestClass.class, descriptor);
final SonarConnection connection = new SonarConnectionMock();
descriptorMapping.onConnect(connection);
descriptorMapping.onDisconnect();
assertThat(descriptor.connected(), equalTo(false));
}
}

View File

@@ -0,0 +1,424 @@
/*
* 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 static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.hasItem;
import static org.hamcrest.MatcherAssert.assertThat;
import android.app.Application;
import android.graphics.Rect;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
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.plugins.console.iface.NullScriptingEnvironment;
import com.facebook.sonar.plugins.console.iface.ScriptingEnvironment;
import com.facebook.sonar.plugins.inspector.InspectorSonarPlugin.TouchOverlayView;
import com.facebook.sonar.plugins.inspector.descriptors.ApplicationDescriptor;
import com.facebook.sonar.testing.SonarConnectionMock;
import com.facebook.sonar.testing.SonarResponderMock;
import com.facebook.testing.robolectric.v3.WithTestDefaultsRunner;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.robolectric.RuntimeEnvironment;
@RunWith(WithTestDefaultsRunner.class)
public class InspectorSonarPluginTest {
private MockApplicationDescriptor mApplicationDescriptor;
private DescriptorMapping mDescriptorMapping;
private ApplicationWrapper mApp;
private ScriptingEnvironment mScriptingEnvironment;
@Before
public void setup() {
final Application app = Mockito.spy(RuntimeEnvironment.application);
Mockito.when(app.getApplicationContext()).thenReturn(app);
Mockito.when(app.getPackageName()).thenReturn("com.facebook.sonar");
mDescriptorMapping = new DescriptorMapping();
mApplicationDescriptor = new MockApplicationDescriptor();
mDescriptorMapping.register(ApplicationWrapper.class, mApplicationDescriptor);
mDescriptorMapping.register(TestNode.class, new TestNodeDescriptor());
mScriptingEnvironment = new NullScriptingEnvironment();
mApp = Mockito.spy(new ApplicationWrapper(app));
}
@Test
public void testOnConnect() throws Exception {
final InspectorSonarPlugin plugin =
new InspectorSonarPlugin(mApp, mDescriptorMapping, mScriptingEnvironment);
final SonarConnection connection = new SonarConnectionMock();
plugin.onConnect(connection);
assertThat(mApplicationDescriptor.connected(), equalTo(true));
}
@Test
public void testOnDisconnect() throws Exception {
final InspectorSonarPlugin plugin =
new InspectorSonarPlugin(mApp, mDescriptorMapping, mScriptingEnvironment);
final SonarConnection connection = new SonarConnectionMock();
plugin.onConnect(connection);
plugin.onDisconnect();
assertThat(mApplicationDescriptor.connected(), equalTo(false));
}
@Test
public void testGetRoot() throws Exception {
final InspectorSonarPlugin plugin =
new InspectorSonarPlugin(mApp, mDescriptorMapping, mScriptingEnvironment);
final SonarResponderMock responder = new SonarResponderMock();
final SonarConnectionMock connection = new SonarConnectionMock();
plugin.onConnect(connection);
final TestNode root = new TestNode();
root.id = "test";
mApplicationDescriptor.root = root;
plugin.mGetRoot.onReceive(null, responder);
assertThat(
responder.successes,
hasItem(
new SonarObject.Builder()
.put("id", "com.facebook.sonar")
.put("name", "com.facebook.sonar")
.put("data", new SonarObject.Builder())
.put("children", new SonarArray.Builder().put("test"))
.put("attributes", new SonarArray.Builder())
.put("decoration", (String) null)
.build()));
}
@Test
public void testGetNodes() throws Exception {
final InspectorSonarPlugin plugin =
new InspectorSonarPlugin(mApp, mDescriptorMapping, mScriptingEnvironment);
final SonarResponderMock responder = new SonarResponderMock();
final SonarConnectionMock connection = new SonarConnectionMock();
plugin.onConnect(connection);
final TestNode root = new TestNode();
root.id = "test";
root.name = "test";
mApplicationDescriptor.root = root;
plugin.mGetRoot.onReceive(null, responder);
plugin.mGetNodes.onReceive(
new SonarObject.Builder().put("ids", new SonarArray.Builder().put("test")).build(),
responder);
assertThat(
responder.successes,
hasItem(
new SonarObject.Builder()
.put(
"elements",
new SonarArray.Builder()
.put(
new SonarObject.Builder()
.put("id", "test")
.put("name", "test")
.put("data", new SonarObject.Builder())
.put("children", new SonarArray.Builder())
.put("attributes", new SonarArray.Builder())
.put("decoration", (String) null)))
.build()));
}
@Test
public void testGetNodesThatDontExist() throws Exception {
final InspectorSonarPlugin plugin =
new InspectorSonarPlugin(mApp, mDescriptorMapping, mScriptingEnvironment);
final SonarResponderMock responder = new SonarResponderMock();
final SonarConnectionMock connection = new SonarConnectionMock();
plugin.onConnect(connection);
final TestNode root = new TestNode();
root.id = "test";
mApplicationDescriptor.root = root;
plugin.mGetRoot.onReceive(null, responder);
plugin.mGetNodes.onReceive(
new SonarObject.Builder().put("ids", new SonarArray.Builder().put("notest")).build(),
responder);
assertThat(
responder.errors,
hasItem(
new SonarObject.Builder()
.put("message", "No node with given id")
.put("id", "notest")
.build()));
}
@Test
public void testSetData() throws Exception {
final InspectorSonarPlugin plugin =
new InspectorSonarPlugin(mApp, mDescriptorMapping, mScriptingEnvironment);
final SonarConnectionMock connection = new SonarConnectionMock();
final SonarResponderMock responder = new SonarResponderMock();
plugin.onConnect(connection);
final TestNode root = new TestNode();
root.id = "test";
root.data = new SonarObject.Builder().put("prop", "value").build();
mApplicationDescriptor.root = root;
plugin.mGetRoot.onReceive(null, responder);
plugin.mSetData.onReceive(
new SonarObject.Builder()
.put("id", "test")
.put("path", new SonarArray.Builder().put("data"))
.put("value", new SonarObject.Builder().put("prop", "updated_value"))
.build(),
responder);
assertThat(root.data.getString("prop"), equalTo("updated_value"));
assertThat(
connection.sent.get("invalidate"),
hasItem(
new SonarObject.Builder()
.put(
"nodes",
new SonarArray.Builder()
.put(new SonarObject.Builder().put("id", "test").build())
.build())
.build()));
}
@Test
public void testSetHighlighted() throws Exception {
final InspectorSonarPlugin plugin =
new InspectorSonarPlugin(mApp, mDescriptorMapping, mScriptingEnvironment);
final SonarConnectionMock connection = new SonarConnectionMock();
final SonarResponderMock responder = new SonarResponderMock();
plugin.onConnect(connection);
final TestNode root = new TestNode();
root.id = "test";
mApplicationDescriptor.root = root;
plugin.mGetRoot.onReceive(null, responder);
plugin.mSetHighlighted.onReceive(
new SonarObject.Builder().put("id", "com.facebook.sonar").build(), responder);
assertThat(mApplicationDescriptor.highlighted, equalTo(true));
plugin.mSetHighlighted.onReceive(
new SonarObject.Builder().put("id", "test").build(), responder);
assertThat(mApplicationDescriptor.highlighted, equalTo(false));
assertThat(root.highlighted, equalTo(true));
plugin.onDisconnect();
assertThat(root.highlighted, equalTo(false));
}
@Test
public void testHitTest() throws Exception {
final InspectorSonarPlugin plugin =
new InspectorSonarPlugin(mApp, mDescriptorMapping, mScriptingEnvironment);
final SonarConnectionMock connection = new SonarConnectionMock();
plugin.onConnect(connection);
final TestNode one = new TestNode();
one.id = "1";
one.bounds.set(5, 5, 20, 20);
final TestNode two = new TestNode();
two.id = "2";
two.bounds.set(20, 20, 100, 100);
final TestNode three = new TestNode();
three.id = "3";
three.bounds.set(0, 0, 20, 20);
final TestNode root = new TestNode();
root.id = "test";
root.children.add(one);
root.children.add(two);
root.children.add(three);
mApplicationDescriptor.root = root;
plugin.hitTest(10, 10);
assertThat(
connection.sent.get("select"),
hasItem(
new SonarObject.Builder()
.put(
"path", new SonarArray.Builder().put("com.facebook.sonar").put("test").put("3"))
.build()));
}
@Test
public void testSetSearchActive() throws Exception {
final InspectorSonarPlugin plugin =
new InspectorSonarPlugin(mApp, mDescriptorMapping, mScriptingEnvironment);
final SonarConnectionMock connection = new SonarConnectionMock();
final SonarResponderMock responder = new SonarResponderMock();
plugin.onConnect(connection);
final ViewGroup decorView = Mockito.spy(new FrameLayout(mApp.getApplication()));
Mockito.when(mApp.getViewRoots()).thenReturn(Arrays.<View>asList(decorView));
plugin.mSetSearchActive.onReceive(
new SonarObject.Builder().put("active", true).build(), responder);
Mockito.verify(decorView, Mockito.times(1)).addView(Mockito.any(TouchOverlayView.class));
plugin.mSetSearchActive.onReceive(
new SonarObject.Builder().put("active", false).build(), responder);
Mockito.verify(decorView, Mockito.times(1)).removeView(Mockito.any(TouchOverlayView.class));
}
@Test(expected = AssertionError.class)
public void testNullChildThrows() throws Exception {
final InspectorSonarPlugin plugin =
new InspectorSonarPlugin(mApp, mDescriptorMapping, mScriptingEnvironment);
final SonarResponderMock responder = new SonarResponderMock();
final SonarConnectionMock connection = new SonarConnectionMock();
plugin.onConnect(connection);
final TestNode root = new TestNode();
root.id = "test";
root.name = "test";
root.children = new ArrayList<>();
root.children.add(null);
mApplicationDescriptor.root = root;
plugin.mGetRoot.onReceive(null, responder);
plugin.mGetNodes.onReceive(
new SonarObject.Builder().put("ids", new SonarArray.Builder().put("test")).build(),
responder);
}
private class TestNode {
String id;
String name;
List<TestNode> children = new ArrayList<>();
SonarObject data;
List<Named<String>> atttributes = new ArrayList<>();
String decoration;
boolean highlighted;
Rect bounds = new Rect();
}
private class TestNodeDescriptor extends NodeDescriptor<TestNode> {
@Override
public void init(TestNode node) {}
@Override
public String getId(TestNode node) {
return node.id;
}
@Override
public String getName(TestNode node) {
return node.name;
}
@Override
public int getChildCount(TestNode node) {
return node.children.size();
}
@Override
public Object getChildAt(TestNode node, int index) {
return node.children.get(index);
}
@Override
public List<Named<SonarObject>> getData(TestNode node) {
return Collections.singletonList(new Named<>("data", node.data));
}
@Override
public void setValue(TestNode node, String[] path, SonarDynamic value) throws Exception {
if (path[0].equals("data")) {
node.data = value.asObject();
}
invalidate(node);
}
@Override
public List<Named<String>> getAttributes(TestNode node) {
return node.atttributes;
}
@Override
public void setHighlighted(TestNode node, boolean selected) {
node.highlighted = selected;
}
@Override
public void hitTest(TestNode node, Touch touch) {
for (int i = node.children.size() - 1; i >= 0; i--) {
final TestNode child = node.children.get(i);
final Rect bounds = child.bounds;
if (touch.containedIn(bounds.left, bounds.top, bounds.right, bounds.bottom)) {
touch.continueWithOffset(i, bounds.left, bounds.top);
return;
}
}
touch.finish();
}
@Override
public String getDecoration(TestNode node) {
return node.decoration;
}
@Override
public boolean matches(String query, TestNode node) {
return getName(node).contains(query);
}
}
private class MockApplicationDescriptor extends ApplicationDescriptor {
TestNode root;
boolean highlighted;
@Override
public int getChildCount(ApplicationWrapper node) {
return 1;
}
@Override
public Object getChildAt(ApplicationWrapper node, int index) {
return root;
}
@Override
public void setHighlighted(ApplicationWrapper node, boolean selected) {
highlighted = selected;
}
@Override
public void hitTest(ApplicationWrapper node, Touch touch) {
touch.continueWithOffset(0, 0, 0);
}
}
}

View File

@@ -0,0 +1,63 @@
/*
* 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.view.View.MeasureSpec.EXACTLY;
import static android.view.View.MeasureSpec.makeMeasureSpec;
import static org.mockito.Matchers.any;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import com.facebook.sonar.plugins.inspector.Touch;
import com.facebook.testing.robolectric.v3.WithTestDefaultsRunner;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.robolectric.RuntimeEnvironment;
@RunWith(WithTestDefaultsRunner.class)
public class ViewGroupDescriptorTest {
@Test
public void testHitTestVisibleChild() {
final ViewGroupDescriptor descriptor = new ViewGroupDescriptor();
final ViewGroup root = new FrameLayout(RuntimeEnvironment.application);
final View child = new View(RuntimeEnvironment.application);
root.addView(child);
root.measure(makeMeasureSpec(100, EXACTLY), makeMeasureSpec(100, EXACTLY));
root.layout(0, 0, 100, 100);
final Touch touch = Mockito.mock(Touch.class);
Mockito.when(touch.containedIn(any(int.class), any(int.class), any(int.class), any(int.class)))
.thenReturn(true);
descriptor.hitTest(root, touch);
Mockito.verify(touch, Mockito.times(1)).continueWithOffset(0, 0, 0);
}
@Test
public void testHitTestInvisibleChild() {
final ViewGroupDescriptor descriptor = new ViewGroupDescriptor();
final ViewGroup root = new FrameLayout(RuntimeEnvironment.application);
final View child = new View(RuntimeEnvironment.application);
child.setVisibility(View.GONE);
root.addView(child);
root.measure(makeMeasureSpec(100, EXACTLY), makeMeasureSpec(100, EXACTLY));
root.layout(0, 0, 100, 100);
final Touch touch = Mockito.mock(Touch.class);
Mockito.when(touch.containedIn(any(int.class), any(int.class), any(int.class), any(int.class)))
.thenReturn(true);
descriptor.hitTest(root, touch);
Mockito.verify(touch, Mockito.times(1)).finish();
}
}

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.doubleconversion">
</manifest>

View File

@@ -0,0 +1,12 @@
cmake_minimum_required (VERSION 3.6.0)
PROJECT(doubleconversion CXX)
enable_language(CXX)
set(PACKAGE_NAME doubleconversion)
set(doubleconversion_DIR double-conversion-3.0.0/double-conversion)
include_directories(${doubleconversion_DIR})
file(GLOB SRCFILES ${doubleconversion_DIR}/*.cc)
message(STATUS "SRC FILES :- " ${SRCFILES})
add_library(${PACKAGE_NAME} SHARED ${SRCFILES})
install(TARGETS ${PACKAGE_NAME} DESTINATION ./build/)
target_link_libraries(${PACKAGE_NAME})

View File

@@ -0,0 +1,34 @@
apply plugin: 'com.android.library'
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 './ApplicationManifest.xml'
}
}
externalNativeBuild {
cmake {
path './CMakeLists.txt'
}
}
}

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.folly">
</manifest>

View File

@@ -0,0 +1,81 @@
cmake_minimum_required (VERSION 3.6.0)
PROJECT(folly CXX)
enable_language(CXX)
set(PACKAGE_NAME folly)
set(FOLLY_DIR ${PROJECT_SOURCE_DIR}/folly)
list(APPEND dir_list ./)
list(APPEND dir_list ${FOLLY_DIR}/lang)
list(APPEND dir_list ${FOLLY_DIR}/hash/)
list(APPEND dir_list ${FOLLY_DIR}/detail)
list(APPEND dir_list ${FOLLY_DIR}/memory/detail)
set(BOOST_DIR ../boost/boost_1_63_0/)
set(GLOG_DIR ../glog/)
set(DOUBLECONVERSION_DIR ../double-conversion/double-conversion-3.0.0/)
list(APPEND dir_list ${BOOST_DIR})
list(APPEND dir_list ${BOOST_DIR}/../)
include_directories(${dir_list})
add_compile_options(
-DFOLLY_NO_CONFIG=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
)
list(APPEND SRC_FILES ${FOLLY_DIR}/Executor.cpp
${FOLLY_DIR}/lang/ColdClass.cpp
${FOLLY_DIR}/lang/Assume.cpp
${FOLLY_DIR}/json.cpp
${FOLLY_DIR}/Unicode.cpp
${FOLLY_DIR}/Conv.cpp
${FOLLY_DIR}/Demangle.cpp
${FOLLY_DIR}/memory/detail/MallocImpl.cpp
${FOLLY_DIR}/String.cpp
${FOLLY_DIR}/dynamic.cpp
${FOLLY_DIR}/ScopeGuard.cpp
${FOLLY_DIR}/json_pointer.cpp
${FOLLY_DIR}/FormatArg.cpp
${FOLLY_DIR}/Format.cpp
)
add_library(${PACKAGE_NAME} SHARED ${SRC_FILES})
set(build_DIR ${CMAKE_SOURCE_DIR}/build)
set(libglog_build_DIR ${build_DIR}/libglog/${ANDROID_ABI})
set(doubleconversion_build_DIR ${build_DIR}/doubleconversion/${ANDROID_ABI})
file(MAKE_DIRECTORY ${build_DIR})
add_subdirectory(${GLOG_DIR} ${libglog_build_DIR})
add_subdirectory(${DOUBLECONVERSION_DIR} ${doubleconversion_build_DIR})
target_include_directories(${PACKAGE_NAME} PRIVATE
${BOOST_DIR}
${BOOST_DIR}/../
${GLOG_DIR}/../
${GLOG_DIR}/glog-0.3.5/src/
${DOUBLECONVERSION_DIR})
install(TARGETS ${PACKAGE_NAME} DESTINATION ./build/)
target_link_libraries(${PACKAGE_NAME} glog double-conversion)

39
android/third-party/Folly/build.gradle vendored Normal file
View File

@@ -0,0 +1,39 @@
apply plugin: 'com.android.library'
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', '-DANDROID_STL=c++_shared'
}
}
}
lintOptions {
abortOnError false
}
sourceSets {
main {
manifest.srcFile './ApplicationManifest.xml'
}
}
externalNativeBuild {
cmake {
path './CMakeLists.txt'
}
}
dependencies {
implementation project(':glog')
implementation project(':doubleconversion')
}
}

Some files were not shown because too many files have changed in this diff Show More