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:
4
android/AndroidManifest.xml
Normal file
4
android/AndroidManifest.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.facebook.sonar">
|
||||
</manifest>
|
||||
63
android/CMakeLists.txt
Normal file
63
android/CMakeLists.txt
Normal file
@@ -0,0 +1,63 @@
|
||||
cmake_minimum_required (VERSION 3.6.0)
|
||||
project(sonar CXX C)
|
||||
set(CMAKE_VERBOSE_MAKEFILE on)
|
||||
|
||||
set(PACKAGE_NAME "sonar")
|
||||
|
||||
add_compile_options(-DFOLLY_NO_CONFIG
|
||||
-DSONAR_JNI_EXTERNAL=1
|
||||
-DFB_SONARKIT_ENABLED=1
|
||||
-DFOLLY_HAVE_MEMRCHR
|
||||
-DFOLLY_MOBILE=1
|
||||
-DFOLLY_USE_LIBCPP=1
|
||||
-DFOLLY_HAVE_LIBJEMALLOC=0
|
||||
-DFOLLY_HAVE_PREADV=0
|
||||
-frtti
|
||||
-fexceptions
|
||||
-std=c++14
|
||||
-Wno-error
|
||||
-Wno-unused-local-typedefs
|
||||
-Wno-unused-variable
|
||||
-Wno-sign-compare
|
||||
-Wno-comment
|
||||
-Wno-return-type
|
||||
-Wno-tautological-constant-compare
|
||||
)
|
||||
|
||||
file(GLOB SOURCES android/sonar.cpp)
|
||||
add_library(${PACKAGE_NAME} SHARED ${SOURCES})
|
||||
target_include_directories(${PACKAGE_NAME} PUBLIC "./")
|
||||
|
||||
set(libjnihack_DIR ${CMAKE_SOURCE_DIR}/../libs/jni-hack/)
|
||||
set(libfbjni_DIR ${CMAKE_SOURCE_DIR}/../libs/fbjni/src/main/cpp/include/)
|
||||
set(libsonar_DIR ${CMAKE_SOURCE_DIR}/../xplat/)
|
||||
set(third_party_ndk build/third-party-ndk)
|
||||
set(libfolly_DIR ${third_party_ndk}/folly/)
|
||||
set(glog_DIR ${third_party_ndk}/glog)
|
||||
set(BOOST_DIR ${third_party_ndk}/boost/boost_1_63_0/)
|
||||
|
||||
|
||||
set(build_DIR ${CMAKE_SOURCE_DIR}/build)
|
||||
|
||||
set(fbjni_build_DIR ${build_DIR}/fbjni/${ANDROID_ABI})
|
||||
set(libsonar_build_DIR ${build_DIR}/libsonar/${ANDROID_ABI})
|
||||
set(libfolly_build_DIR ${build_DIR}/libfolly/${ANDROID_ABI})
|
||||
|
||||
file(MAKE_DIRECTORY ${build_DIR})
|
||||
|
||||
add_subdirectory(${libsonar_DIR} ${libsonar_build_DIR})
|
||||
add_subdirectory(${libfbjni_DIR}/../ ${fbjni_build_DIR})
|
||||
|
||||
target_include_directories(${PACKAGE_NAME} PRIVATE
|
||||
${libjnihack_DIR}
|
||||
${libfbjni_DIR}
|
||||
${libsonar_DIR}
|
||||
${libfolly_DIR}
|
||||
${glog_DIR}
|
||||
${glog_DIR}/../
|
||||
${glog_DIR}/glog-0.3.5/src/
|
||||
${BOOST_DIR}
|
||||
${BOOST_DIR}/../
|
||||
)
|
||||
|
||||
target_link_libraries(${PACKAGE_NAME} fb sonarcpp)
|
||||
83
android/android/AndroidSonarClient.java
Normal file
83
android/android/AndroidSonarClient.java
Normal file
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
package com.facebook.sonar.android;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.wifi.WifiInfo;
|
||||
import android.net.wifi.WifiManager;
|
||||
import android.os.Build;
|
||||
import com.facebook.sonar.core.SonarClient;
|
||||
|
||||
public final class AndroidSonarClient {
|
||||
private static boolean sIsInitialized = false;
|
||||
private static SonarThread sSonarThread;
|
||||
|
||||
public static synchronized SonarClient getInstance(Context context) {
|
||||
if (!sIsInitialized) {
|
||||
sSonarThread = new SonarThread();
|
||||
sSonarThread.start();
|
||||
|
||||
final Context app = context.getApplicationContext();
|
||||
SonarClientImpl.init(
|
||||
sSonarThread.getEventBase(),
|
||||
getServerHost(app),
|
||||
"Android",
|
||||
getFriendlyDeviceName(),
|
||||
getId(),
|
||||
getRunningAppName(app),
|
||||
getPackageName(app),
|
||||
context.getFilesDir().getAbsolutePath());
|
||||
sIsInitialized = true;
|
||||
}
|
||||
return SonarClientImpl.getInstance();
|
||||
}
|
||||
|
||||
static boolean isRunningOnGenymotion() {
|
||||
return Build.FINGERPRINT.contains("vbox");
|
||||
}
|
||||
|
||||
static boolean isRunningOnStockEmulator() {
|
||||
return Build.FINGERPRINT.contains("generic") && !Build.FINGERPRINT.contains("vbox");
|
||||
}
|
||||
|
||||
static String getId() {
|
||||
return Build.SERIAL;
|
||||
}
|
||||
|
||||
static String getFriendlyDeviceName() {
|
||||
if (isRunningOnGenymotion()) {
|
||||
// Genymotion already has a friendly name by default
|
||||
return Build.MODEL;
|
||||
} else {
|
||||
return Build.MODEL + " - " + Build.VERSION.RELEASE + " - API " + Build.VERSION.SDK_INT;
|
||||
}
|
||||
}
|
||||
|
||||
static String getServerHost(Context context) {
|
||||
if (isRunningOnStockEmulator()) {
|
||||
return "10.0.2.2";
|
||||
} else if (isRunningOnGenymotion()) {
|
||||
// This is hand-wavy but works on but ipv4 and ipv6 genymotion
|
||||
final WifiManager wifi = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
|
||||
final WifiInfo info = wifi.getConnectionInfo();
|
||||
final int ip = info.getIpAddress();
|
||||
return String.format("%d.%d.%d.2", (ip & 0xff), (ip >> 8 & 0xff), (ip >> 16 & 0xff));
|
||||
} else {
|
||||
// Running on physical device. Sonar desktop will run `adb reverse tcp:8088 tcp:8088`
|
||||
return "localhost";
|
||||
}
|
||||
}
|
||||
|
||||
static String getRunningAppName(Context context) {
|
||||
return context.getApplicationInfo().loadLabel(context.getPackageManager()).toString();
|
||||
}
|
||||
|
||||
static String getPackageName(Context context) {
|
||||
return context.getPackageName();
|
||||
}
|
||||
}
|
||||
32
android/android/EventBase.java
Normal file
32
android/android/EventBase.java
Normal file
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright (c) 2004-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
package com.facebook.sonar.android;
|
||||
|
||||
import com.facebook.jni.HybridClassBase;
|
||||
import com.facebook.proguard.annotations.DoNotStrip;
|
||||
import com.facebook.soloader.SoLoader;
|
||||
import com.facebook.sonar.BuildConfig;
|
||||
|
||||
@DoNotStrip
|
||||
class EventBase extends HybridClassBase {
|
||||
static {
|
||||
if (BuildConfig.IS_INTERNAL_BUILD) {
|
||||
SoLoader.loadLibrary("sonar");
|
||||
}
|
||||
}
|
||||
|
||||
EventBase() {
|
||||
initHybrid();
|
||||
}
|
||||
|
||||
@DoNotStrip
|
||||
native void loopForever();
|
||||
|
||||
@DoNotStrip
|
||||
private native void initHybrid();
|
||||
}
|
||||
57
android/android/SonarClientImpl.java
Normal file
57
android/android/SonarClientImpl.java
Normal file
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
package com.facebook.sonar.android;
|
||||
|
||||
import com.facebook.jni.HybridData;
|
||||
import com.facebook.proguard.annotations.DoNotStrip;
|
||||
import com.facebook.soloader.SoLoader;
|
||||
import com.facebook.sonar.BuildConfig;
|
||||
import com.facebook.sonar.core.SonarClient;
|
||||
import com.facebook.sonar.core.SonarPlugin;
|
||||
|
||||
@DoNotStrip
|
||||
class SonarClientImpl implements SonarClient {
|
||||
static {
|
||||
if (BuildConfig.IS_INTERNAL_BUILD) {
|
||||
SoLoader.loadLibrary("sonar");
|
||||
}
|
||||
}
|
||||
|
||||
private final HybridData mHybridData;
|
||||
|
||||
private SonarClientImpl(HybridData hd) {
|
||||
mHybridData = hd;
|
||||
}
|
||||
|
||||
public static native void init(
|
||||
EventBase eventBase,
|
||||
String host,
|
||||
String os,
|
||||
String device,
|
||||
String deviceId,
|
||||
String app,
|
||||
String appId,
|
||||
String privateAppDirectory);
|
||||
|
||||
public static native SonarClientImpl getInstance();
|
||||
|
||||
@Override
|
||||
public native void addPlugin(SonarPlugin plugin);
|
||||
|
||||
@Override
|
||||
public native <T extends SonarPlugin> T getPlugin(String id);
|
||||
|
||||
@Override
|
||||
public native void removePlugin(SonarPlugin plugin);
|
||||
|
||||
@Override
|
||||
public native void start();
|
||||
|
||||
@Override
|
||||
public native void stop();
|
||||
}
|
||||
52
android/android/SonarConnectionImpl.java
Normal file
52
android/android/SonarConnectionImpl.java
Normal file
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
package com.facebook.sonar.android;
|
||||
|
||||
import com.facebook.jni.HybridData;
|
||||
import com.facebook.proguard.annotations.DoNotStrip;
|
||||
import com.facebook.soloader.SoLoader;
|
||||
import com.facebook.sonar.BuildConfig;
|
||||
import com.facebook.sonar.core.SonarArray;
|
||||
import com.facebook.sonar.core.SonarConnection;
|
||||
import com.facebook.sonar.core.SonarObject;
|
||||
import com.facebook.sonar.core.SonarReceiver;
|
||||
|
||||
@DoNotStrip
|
||||
class SonarConnectionImpl implements SonarConnection {
|
||||
static {
|
||||
if (BuildConfig.IS_INTERNAL_BUILD) {
|
||||
SoLoader.loadLibrary("sonar");
|
||||
}
|
||||
}
|
||||
|
||||
private final HybridData mHybridData;
|
||||
|
||||
private SonarConnectionImpl(HybridData hd) {
|
||||
mHybridData = hd;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void send(String method, SonarObject params) {
|
||||
sendObject(method, params);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void send(String method, SonarArray params) {
|
||||
sendArray(method, params);
|
||||
}
|
||||
|
||||
public native void sendObject(String method, SonarObject params);
|
||||
|
||||
public native void sendArray(String method, SonarArray params);
|
||||
|
||||
@Override
|
||||
public native void reportError(Throwable throwable);
|
||||
|
||||
@Override
|
||||
public native void receive(String method, SonarReceiver receiver);
|
||||
}
|
||||
53
android/android/SonarResponderImpl.java
Normal file
53
android/android/SonarResponderImpl.java
Normal file
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
package com.facebook.sonar.android;
|
||||
|
||||
import com.facebook.jni.HybridData;
|
||||
import com.facebook.proguard.annotations.DoNotStrip;
|
||||
import com.facebook.soloader.SoLoader;
|
||||
import com.facebook.sonar.BuildConfig;
|
||||
import com.facebook.sonar.core.SonarArray;
|
||||
import com.facebook.sonar.core.SonarObject;
|
||||
import com.facebook.sonar.core.SonarResponder;
|
||||
|
||||
@DoNotStrip
|
||||
class SonarResponderImpl implements SonarResponder {
|
||||
static {
|
||||
if (BuildConfig.IS_INTERNAL_BUILD) {
|
||||
SoLoader.loadLibrary("sonar");
|
||||
}
|
||||
}
|
||||
|
||||
private final HybridData mHybridData;
|
||||
|
||||
private SonarResponderImpl(HybridData hd) {
|
||||
mHybridData = hd;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void success(SonarObject params) {
|
||||
successObject(params);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void success(SonarArray params) {
|
||||
successArray(params);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void success() {
|
||||
successObject(new SonarObject.Builder().build());
|
||||
}
|
||||
|
||||
public native void successObject(SonarObject response);
|
||||
|
||||
public native void successArray(SonarArray response);
|
||||
|
||||
@Override
|
||||
public native void error(SonarObject response);
|
||||
}
|
||||
44
android/android/SonarThread.java
Normal file
44
android/android/SonarThread.java
Normal file
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright (c) 2004-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
package com.facebook.sonar.android;
|
||||
|
||||
import android.os.Process;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
class SonarThread extends Thread {
|
||||
private @Nullable EventBase mEventBase;
|
||||
|
||||
SonarThread() {
|
||||
super("SonarEventBaseThread");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
|
||||
synchronized (this) {
|
||||
try {
|
||||
mEventBase = new EventBase();
|
||||
} finally {
|
||||
notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
mEventBase.loopForever();
|
||||
}
|
||||
|
||||
synchronized EventBase getEventBase() {
|
||||
while (mEventBase == null) {
|
||||
try {
|
||||
wait();
|
||||
} catch (InterruptedException e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
return mEventBase;
|
||||
}
|
||||
}
|
||||
305
android/android/sonar.cpp
Normal file
305
android/android/sonar.cpp
Normal file
@@ -0,0 +1,305 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <fb/fbjni.h>
|
||||
|
||||
#include <folly/json.h>
|
||||
#include <folly/io/async/EventBase.h>
|
||||
#include <folly/io/async/EventBaseManager.h>
|
||||
|
||||
#include <Sonar/SonarClient.h>
|
||||
#include <Sonar/SonarWebSocket.h>
|
||||
#include <Sonar/SonarConnection.h>
|
||||
#include <Sonar/SonarResponder.h>
|
||||
|
||||
using namespace facebook;
|
||||
using namespace facebook::sonar;
|
||||
|
||||
namespace {
|
||||
|
||||
class JEventBase : public jni::HybridClass<JEventBase> {
|
||||
public:
|
||||
constexpr static auto kJavaDescriptor = "Lcom/facebook/sonar/android/EventBase;";
|
||||
|
||||
static void registerNatives() {
|
||||
registerHybrid({
|
||||
makeNativeMethod("initHybrid", JEventBase::initHybrid),
|
||||
makeNativeMethod("loopForever", JEventBase::loopForever),
|
||||
});
|
||||
}
|
||||
|
||||
folly::EventBase* eventBase() {
|
||||
return &eventBase_;
|
||||
}
|
||||
|
||||
private:
|
||||
friend HybridBase;
|
||||
|
||||
JEventBase() {}
|
||||
|
||||
void loopForever() {
|
||||
folly::EventBaseManager::get()->setEventBase(&eventBase_, false);
|
||||
eventBase_.loopForever();
|
||||
}
|
||||
|
||||
static void initHybrid(jni::alias_ref<jhybridobject> o) {
|
||||
return setCxxInstance(o);
|
||||
}
|
||||
|
||||
folly::EventBase eventBase_;
|
||||
};
|
||||
|
||||
class JSonarObject : public jni::JavaClass<JSonarObject> {
|
||||
public:
|
||||
constexpr static auto kJavaDescriptor = "Lcom/facebook/sonar/core/SonarObject;";
|
||||
|
||||
static jni::local_ref<JSonarObject> create(const folly::dynamic& json) {
|
||||
return newInstance(folly::toJson(json));
|
||||
}
|
||||
|
||||
std::string toJsonString() {
|
||||
static const auto method = javaClassStatic()->getMethod<std::string()>("toJsonString");
|
||||
return method(self())->toStdString();
|
||||
}
|
||||
};
|
||||
|
||||
class JSonarArray : public jni::JavaClass<JSonarArray> {
|
||||
public:
|
||||
constexpr static auto kJavaDescriptor = "Lcom/facebook/sonar/core/SonarArray;";
|
||||
|
||||
static jni::local_ref<JSonarArray> create(const folly::dynamic& json) {
|
||||
return newInstance(folly::toJson(json));
|
||||
}
|
||||
|
||||
std::string toJsonString() {
|
||||
static const auto method = javaClassStatic()->getMethod<std::string()>("toJsonString");
|
||||
return method(self())->toStdString();
|
||||
}
|
||||
};
|
||||
|
||||
class JSonarResponder : public jni::JavaClass<JSonarResponder> {
|
||||
public:
|
||||
constexpr static auto kJavaDescriptor = "Lcom/facebook/sonar/core/SonarResponder;";
|
||||
};
|
||||
|
||||
class JSonarResponderImpl : public jni::HybridClass<JSonarResponderImpl, JSonarResponder> {
|
||||
public:
|
||||
constexpr static auto kJavaDescriptor = "Lcom/facebook/sonar/android/SonarResponderImpl;";
|
||||
|
||||
static void registerNatives() {
|
||||
registerHybrid({
|
||||
makeNativeMethod("successObject", JSonarResponderImpl::successObject),
|
||||
makeNativeMethod("successArray", JSonarResponderImpl::successArray),
|
||||
makeNativeMethod("error", JSonarResponderImpl::error),
|
||||
});
|
||||
}
|
||||
|
||||
void successObject(jni::alias_ref<JSonarObject> json) {
|
||||
_responder->success(json ? folly::parseJson(json->toJsonString()) : folly::dynamic::object());
|
||||
}
|
||||
|
||||
void successArray(jni::alias_ref<JSonarArray> json) {
|
||||
_responder->success(json ? folly::parseJson(json->toJsonString()) : folly::dynamic::object());
|
||||
}
|
||||
|
||||
void error(jni::alias_ref<JSonarObject> json) {
|
||||
_responder->error(json ? folly::parseJson(json->toJsonString()) : folly::dynamic::object());
|
||||
}
|
||||
|
||||
private:
|
||||
friend HybridBase;
|
||||
std::shared_ptr<SonarResponder> _responder;
|
||||
|
||||
JSonarResponderImpl(std::shared_ptr<SonarResponder> responder): _responder(std::move(responder)) {}
|
||||
};
|
||||
|
||||
class JSonarReceiver : public jni::JavaClass<JSonarReceiver> {
|
||||
public:
|
||||
constexpr static auto kJavaDescriptor = "Lcom/facebook/sonar/core/SonarReceiver;";
|
||||
|
||||
void receive(const folly::dynamic params, std::shared_ptr<SonarResponder> responder) const {
|
||||
static const auto method = javaClassStatic()->getMethod<void(jni::alias_ref<JSonarObject::javaobject>, jni::alias_ref<JSonarResponder::javaobject>)>("onReceive");
|
||||
method(self(), JSonarObject::create(std::move(params)), JSonarResponderImpl::newObjectCxxArgs(responder));
|
||||
}
|
||||
};
|
||||
|
||||
class JSonarConnection : public jni::JavaClass<JSonarConnection> {
|
||||
public:
|
||||
constexpr static auto kJavaDescriptor = "Lcom/facebook/sonar/core/SonarConnection;";
|
||||
};
|
||||
|
||||
class JSonarConnectionImpl : public jni::HybridClass<JSonarConnectionImpl, JSonarConnection> {
|
||||
public:
|
||||
constexpr static auto kJavaDescriptor = "Lcom/facebook/sonar/android/SonarConnectionImpl;";
|
||||
|
||||
static void registerNatives() {
|
||||
registerHybrid({
|
||||
makeNativeMethod("sendObject", JSonarConnectionImpl::sendObject),
|
||||
makeNativeMethod("sendArray", JSonarConnectionImpl::sendArray),
|
||||
makeNativeMethod("reportError", JSonarConnectionImpl::reportError),
|
||||
makeNativeMethod("receive", JSonarConnectionImpl::receive),
|
||||
});
|
||||
}
|
||||
|
||||
void sendObject(const std::string method, jni::alias_ref<JSonarObject> json) {
|
||||
_connection->send(std::move(method), json ? folly::parseJson(json->toJsonString()) : folly::dynamic::object());
|
||||
}
|
||||
|
||||
void sendArray(const std::string method, jni::alias_ref<JSonarArray> json) {
|
||||
_connection->send(std::move(method), json ? folly::parseJson(json->toJsonString()) : folly::dynamic::object());
|
||||
}
|
||||
|
||||
void reportError(jni::alias_ref<jni::JThrowable> throwable) {
|
||||
_connection->error(throwable->toString(), throwable->getStackTrace()->toString());
|
||||
}
|
||||
|
||||
void receive(const std::string method, jni::alias_ref<JSonarReceiver> receiver) {
|
||||
auto global = make_global(receiver);
|
||||
_connection->receive(std::move(method), [global] (const folly::dynamic& params, std::unique_ptr<SonarResponder> responder) {
|
||||
global->receive(params, std::move(responder));
|
||||
});
|
||||
}
|
||||
|
||||
private:
|
||||
friend HybridBase;
|
||||
std::shared_ptr<SonarConnection> _connection;
|
||||
|
||||
JSonarConnectionImpl(std::shared_ptr<SonarConnection> connection): _connection(std::move(connection)) {}
|
||||
};
|
||||
|
||||
class JSonarPlugin : public jni::JavaClass<JSonarPlugin> {
|
||||
public:
|
||||
constexpr static auto kJavaDescriptor = "Lcom/facebook/sonar/core/SonarPlugin;";
|
||||
|
||||
std::string identifier() const {
|
||||
static const auto method = javaClassStatic()->getMethod<std::string()>("getId");
|
||||
return method(self())->toStdString();
|
||||
}
|
||||
|
||||
void didConnect(std::shared_ptr<SonarConnection> conn) {
|
||||
static const auto method = javaClassStatic()->getMethod<void(jni::alias_ref<JSonarConnection::javaobject>)>("onConnect");
|
||||
method(self(), JSonarConnectionImpl::newObjectCxxArgs(conn));
|
||||
}
|
||||
|
||||
void didDisconnect() {
|
||||
static const auto method = javaClassStatic()->getMethod<void()>("onDisconnect");
|
||||
method(self());
|
||||
}
|
||||
};
|
||||
|
||||
class JSonarPluginWrapper : public SonarPlugin {
|
||||
public:
|
||||
jni::global_ref<JSonarPlugin> jplugin;
|
||||
|
||||
virtual std::string identifier() const override {
|
||||
return jplugin->identifier();
|
||||
}
|
||||
|
||||
virtual void didConnect(std::shared_ptr<SonarConnection> conn) override {
|
||||
jplugin->didConnect(conn);
|
||||
}
|
||||
|
||||
virtual void didDisconnect() override {
|
||||
jplugin->didDisconnect();
|
||||
}
|
||||
|
||||
JSonarPluginWrapper(jni::global_ref<JSonarPlugin> plugin): jplugin(plugin) {}
|
||||
};
|
||||
|
||||
class JSonarClient : public jni::HybridClass<JSonarClient> {
|
||||
public:
|
||||
constexpr static auto kJavaDescriptor = "Lcom/facebook/sonar/android/SonarClientImpl;";
|
||||
|
||||
static void registerNatives() {
|
||||
registerHybrid({
|
||||
makeNativeMethod("init", JSonarClient::init),
|
||||
makeNativeMethod("getInstance", JSonarClient::getInstance),
|
||||
makeNativeMethod("start", JSonarClient::start),
|
||||
makeNativeMethod("stop", JSonarClient::stop),
|
||||
makeNativeMethod("addPlugin", JSonarClient::addPlugin),
|
||||
makeNativeMethod("removePlugin", JSonarClient::removePlugin),
|
||||
makeNativeMethod("getPlugin", JSonarClient::getPlugin),
|
||||
});
|
||||
}
|
||||
|
||||
static jni::alias_ref<JSonarClient::javaobject> getInstance(jni::alias_ref<jclass>) {
|
||||
static auto client = make_global(newObjectCxxArgs());
|
||||
return client;
|
||||
}
|
||||
|
||||
void start() {
|
||||
SonarClient::instance()->start();
|
||||
}
|
||||
|
||||
void stop() {
|
||||
SonarClient::instance()->stop();
|
||||
}
|
||||
|
||||
void addPlugin(jni::alias_ref<JSonarPlugin> plugin) {
|
||||
auto wrapper = std::make_shared<JSonarPluginWrapper>(make_global(plugin));
|
||||
SonarClient::instance()->addPlugin(wrapper);
|
||||
}
|
||||
|
||||
void removePlugin(jni::alias_ref<JSonarPlugin> plugin) {
|
||||
auto client = SonarClient::instance();
|
||||
client->removePlugin(client->getPlugin(plugin->identifier()));
|
||||
}
|
||||
|
||||
jni::alias_ref<JSonarPlugin> getPlugin(const std::string& identifier) {
|
||||
auto plugin = SonarClient::instance()->getPlugin(identifier);
|
||||
if (plugin) {
|
||||
auto wrapper = std::static_pointer_cast<JSonarPluginWrapper>(plugin);
|
||||
return wrapper->jplugin;
|
||||
} else {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
static void init(
|
||||
jni::alias_ref<jclass>,
|
||||
JEventBase* jEventBase,
|
||||
const std::string host,
|
||||
const std::string os,
|
||||
const std::string device,
|
||||
const std::string deviceId,
|
||||
const std::string app,
|
||||
const std::string appId,
|
||||
const std::string privateAppDirectory) {
|
||||
|
||||
SonarClient::init({
|
||||
{
|
||||
std::move(host),
|
||||
std::move(os),
|
||||
std::move(device),
|
||||
std::move(deviceId),
|
||||
std::move(app),
|
||||
std::move(appId),
|
||||
std::move(privateAppDirectory)
|
||||
},
|
||||
jEventBase->eventBase(),
|
||||
});
|
||||
}
|
||||
|
||||
private:
|
||||
friend HybridBase;
|
||||
|
||||
JSonarClient() {}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
jint JNI_OnLoad(JavaVM* vm, void*) {
|
||||
return jni::initialize(vm, [] {
|
||||
JSonarClient::registerNatives();
|
||||
JSonarConnectionImpl::registerNatives();
|
||||
JSonarResponderImpl::registerNatives();
|
||||
JEventBase::registerNatives();
|
||||
});
|
||||
}
|
||||
54
android/android/utils/SonarUtils.java
Normal file
54
android/android/utils/SonarUtils.java
Normal file
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
package com.facebook.sonar.android.utils;
|
||||
|
||||
import android.app.ActivityManager;
|
||||
import android.content.Context;
|
||||
import com.facebook.sonar.BuildConfig;
|
||||
import java.util.List;
|
||||
|
||||
public final class SonarUtils {
|
||||
|
||||
private SonarUtils() {}
|
||||
|
||||
public static boolean shouldEnableSonar(Context context) {
|
||||
return BuildConfig.IS_INTERNAL_BUILD && !isEndToEndTest() && isMainProcess(context);
|
||||
}
|
||||
|
||||
private static boolean isEndToEndTest() {
|
||||
final String value = System.getenv("BUDDY_SONAR_DISABLED");
|
||||
if (value == null || value.length() == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return Boolean.parseBoolean(value);
|
||||
} catch (NumberFormatException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isMainProcess(Context context) {
|
||||
final int pid = android.os.Process.myPid();
|
||||
final ActivityManager manager =
|
||||
(ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
|
||||
final List<ActivityManager.RunningAppProcessInfo> infoList = manager.getRunningAppProcesses();
|
||||
|
||||
String processName = null;
|
||||
if (infoList != null) {
|
||||
for (ActivityManager.RunningAppProcessInfo info : infoList) {
|
||||
if (info.pid == pid) {
|
||||
processName = info.processName;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return context.getPackageName().equals(processName);
|
||||
}
|
||||
}
|
||||
172
android/build.gradle
Normal file
172
android/build.gradle
Normal file
@@ -0,0 +1,172 @@
|
||||
apply plugin: 'com.android.library'
|
||||
|
||||
apply plugin: 'de.undercouch.download'
|
||||
|
||||
import de.undercouch.gradle.tasks.download.Download
|
||||
import org.apache.tools.ant.taskdefs.condition.Os
|
||||
import org.apache.tools.ant.filters.ReplaceTokens
|
||||
|
||||
def downloadsDir = new File("$buildDir/downloads")
|
||||
def thirdPartyNdkDir = new File("$buildDir/third-party-ndk")
|
||||
|
||||
task createNativeDepsDirectories {
|
||||
downloadsDir.mkdirs()
|
||||
thirdPartyNdkDir.mkdirs()
|
||||
}
|
||||
|
||||
task downloadGlog(dependsOn: createNativeDepsDirectories, type: Download) {
|
||||
src 'https://github.com/google/glog/archive/v0.3.5.tar.gz'
|
||||
onlyIfNewer true
|
||||
overwrite false
|
||||
dest new File(downloadsDir, 'glog-0.3.5.tar.gz')
|
||||
}
|
||||
|
||||
task prepareGlog(dependsOn: [downloadGlog], type: Copy) {
|
||||
from tarTree(downloadGlog.dest)
|
||||
from './third-party/glog/'
|
||||
include 'glog-0.3.5/src/**/*', 'Android.mk', 'config.h', 'build.gradle', 'CMakeLists.txt', 'ApplicationManifest.xml'
|
||||
includeEmptyDirs = false
|
||||
filesMatching('**/*.h.in') {
|
||||
filter(ReplaceTokens, tokens: [
|
||||
ac_cv_have_unistd_h: '1',
|
||||
ac_cv_have_stdint_h: '1',
|
||||
ac_cv_have_systypes_h: '1',
|
||||
ac_cv_have_inttypes_h: '1',
|
||||
ac_cv_have_libgflags: '0',
|
||||
ac_google_start_namespace: 'namespace google {',
|
||||
ac_cv_have_uint16_t: '1',
|
||||
ac_cv_have_u_int16_t: '1',
|
||||
ac_cv_have___uint16: '0',
|
||||
ac_google_end_namespace: '}',
|
||||
ac_cv_have___builtin_expect: '1',
|
||||
ac_google_namespace: 'google',
|
||||
ac_cv___attribute___noinline: '__attribute__ ((noinline))',
|
||||
ac_cv___attribute___noreturn: '__attribute__ ((noreturn))',
|
||||
ac_cv___attribute___printf_4_5: '__attribute__((__format__ (__printf__, 4, 5)))'
|
||||
])
|
||||
it.path = (it.name - '.in')
|
||||
}
|
||||
into "$thirdPartyNdkDir/glog"
|
||||
}
|
||||
|
||||
task finalizeGlog(dependsOn: [prepareGlog], type: Copy) {
|
||||
from './third-party/glog/'
|
||||
include 'logging.cc'
|
||||
includeEmptyDirs = false
|
||||
into "$thirdPartyNdkDir/glog/glog-0.3.5/src/"
|
||||
}
|
||||
|
||||
task downloadDoubleConversion(dependsOn: createNativeDepsDirectories, type: Download) {
|
||||
src 'https://github.com/google/double-conversion/archive/v3.0.0.tar.gz'
|
||||
onlyIfNewer true
|
||||
overwrite false
|
||||
dest new File(downloadsDir, 'double-conversion-3.0.0.tar.gz')
|
||||
}
|
||||
|
||||
task prepareDoubleConversion(dependsOn: [downloadDoubleConversion], type: Copy) {
|
||||
from tarTree(downloadDoubleConversion.dest)
|
||||
from './third-party/DoubleConversion/'
|
||||
include 'double-conversion-3.0.0/**/*', 'build.gradle', 'CMakeLists.txt', 'ApplicationManifest.xml'
|
||||
includeEmptyDirs = false
|
||||
into "$thirdPartyNdkDir/double-conversion"
|
||||
}
|
||||
|
||||
task downloadBoost(dependsOn: createNativeDepsDirectories, type: Download) {
|
||||
src 'https://github.com/react-native-community/boost-for-react-native/releases/download/v1.63.0-0/boost_1_63_0.tar.gz'
|
||||
onlyIfNewer true
|
||||
overwrite false
|
||||
dest new File(downloadsDir, 'boost_1_63_0.tar.gz')
|
||||
}
|
||||
|
||||
task prepareBoost(dependsOn: [downloadBoost], type: Copy) {
|
||||
from tarTree(resources.gzip(downloadBoost.dest))
|
||||
include 'boost_1_63_0/boost/**/*.hpp', 'boost/boost/**/*.hpp'
|
||||
includeEmptyDirs = false
|
||||
into "$thirdPartyNdkDir/boost"
|
||||
doLast {
|
||||
file("$thirdPartyNdkDir/boost/boost").renameTo("$thirdPartyNdkDir/boost/boost_1_63_0")
|
||||
}
|
||||
}
|
||||
|
||||
task downloadFolly(dependsOn: createNativeDepsDirectories, type: Download) {
|
||||
src 'https://github.com/facebook/folly/archive/v2018.05.21.00.tar.gz'
|
||||
onlyIfNewer true
|
||||
overwrite false
|
||||
dest new File(downloadsDir, 'folly-2018.05.21.00.tar.gz');
|
||||
}
|
||||
|
||||
task prepareFolly(dependsOn: [downloadFolly], type: Copy) {
|
||||
from tarTree(downloadFolly.dest)
|
||||
from './third-party/folly/'
|
||||
include 'folly-2018.05.21.00/folly/**/*', 'build.gradle', 'CMakeLists.txt', 'ApplicationManifest.xml'
|
||||
eachFile {fname -> fname.path = (fname.path - "folly-2018.05.21.00/")}
|
||||
includeEmptyDirs = false
|
||||
into "$thirdPartyNdkDir/folly"
|
||||
}
|
||||
|
||||
task prepareAllLibs() {
|
||||
dependsOn finalizeGlog
|
||||
dependsOn prepareDoubleConversion
|
||||
dependsOn prepareBoost
|
||||
dependsOn prepareFolly
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion rootProject.compileSdkVersion
|
||||
buildToolsVersion rootProject.buildToolsVersion
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion rootProject.minSdkVersion
|
||||
targetSdkVersion rootProject.targetSdkVersion
|
||||
buildConfigField "boolean", "IS_INTERNAL_BUILD", 'true'
|
||||
ndk {
|
||||
abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'
|
||||
}
|
||||
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
arguments '-DANDROID_TOOLCHAIN=clang'
|
||||
}
|
||||
}
|
||||
}
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
}
|
||||
sourceSets {
|
||||
main {
|
||||
manifest.srcFile './AndroidManifest.xml'
|
||||
java {
|
||||
srcDir 'android'
|
||||
srcDir 'core'
|
||||
srcDir 'plugins'
|
||||
}
|
||||
res {
|
||||
srcDir 'res'
|
||||
}
|
||||
}
|
||||
}
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
path './CMakeLists.txt'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(':fbjni')
|
||||
implementation deps.soloader
|
||||
compileOnly deps.lithoAnnotations
|
||||
implementation 'org.glassfish:javax.annotation:10.0-b28'
|
||||
implementation deps.guava
|
||||
implementation deps.jsr305
|
||||
implementation deps.supportAppCompat
|
||||
implementation deps.stetho
|
||||
implementation deps.okhttp3
|
||||
implementation 'com.facebook.litho:litho-core:0.15.0'
|
||||
implementation 'com.facebook.litho:litho-widget:0.15.0'
|
||||
implementation fileTree(dir: 'plugins/console/dependencies', include: ['*.jar'])
|
||||
}
|
||||
}
|
||||
|
||||
project.afterEvaluate {
|
||||
preBuild.dependsOn prepareAllLibs
|
||||
}
|
||||
34
android/core/ErrorReportingRunnable.java
Normal file
34
android/core/ErrorReportingRunnable.java
Normal file
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
package com.facebook.sonar.core;
|
||||
|
||||
public abstract class ErrorReportingRunnable implements Runnable {
|
||||
|
||||
private final SonarConnection mConnection;
|
||||
|
||||
public ErrorReportingRunnable(SonarConnection connection) {
|
||||
mConnection = connection;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void run() {
|
||||
try {
|
||||
runOrThrow();
|
||||
} catch (Exception e) {
|
||||
if (mConnection != null) {
|
||||
mConnection.reportError(e);
|
||||
}
|
||||
} finally {
|
||||
doFinally();
|
||||
}
|
||||
}
|
||||
|
||||
protected void doFinally() {}
|
||||
|
||||
protected abstract void runOrThrow() throws Exception;
|
||||
}
|
||||
164
android/core/SonarArray.java
Normal file
164
android/core/SonarArray.java
Normal file
@@ -0,0 +1,164 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
package com.facebook.sonar.core;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
public class SonarArray {
|
||||
final JSONArray mJson;
|
||||
|
||||
SonarArray(JSONArray json) {
|
||||
mJson = (json != null ? json : new JSONArray());
|
||||
}
|
||||
|
||||
SonarArray(String json) {
|
||||
try {
|
||||
mJson = new JSONArray(json);
|
||||
} catch (JSONException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public SonarDynamic getDynamic(int index) {
|
||||
return new SonarDynamic(mJson.opt(index));
|
||||
}
|
||||
|
||||
public String getString(int index) {
|
||||
return mJson.optString(index);
|
||||
}
|
||||
|
||||
public int getInt(int index) {
|
||||
return mJson.optInt(index);
|
||||
}
|
||||
|
||||
public long getLong(int index) {
|
||||
return mJson.optLong(index);
|
||||
}
|
||||
|
||||
public float getFloat(int index) {
|
||||
return (float) mJson.optDouble(index);
|
||||
}
|
||||
|
||||
public double getDouble(int index) {
|
||||
return mJson.optDouble(index);
|
||||
}
|
||||
|
||||
public boolean getBoolean(int index) {
|
||||
return mJson.optBoolean(index);
|
||||
}
|
||||
|
||||
public SonarObject getObject(int index) {
|
||||
final Object o = mJson.opt(index);
|
||||
return new SonarObject((JSONObject) o);
|
||||
}
|
||||
|
||||
public SonarArray getArray(int index) {
|
||||
final Object o = mJson.opt(index);
|
||||
return new SonarArray((JSONArray) o);
|
||||
}
|
||||
|
||||
public int length() {
|
||||
return mJson.length();
|
||||
}
|
||||
|
||||
public List<String> toStringList() {
|
||||
final int length = length();
|
||||
final List<String> list = new ArrayList<>(length);
|
||||
for (int i = 0; i < length; i++) {
|
||||
list.add(getString(i));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
public String toJsonString() {
|
||||
return toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return mJson.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
return mJson.toString().equals(o.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return mJson.hashCode();
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
private final JSONArray mJson;
|
||||
|
||||
public Builder() {
|
||||
mJson = new JSONArray();
|
||||
}
|
||||
|
||||
public Builder put(String s) {
|
||||
mJson.put(s);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder put(Integer i) {
|
||||
mJson.put(i);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder put(Long l) {
|
||||
mJson.put(l);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder put(Float f) {
|
||||
mJson.put(Float.isNaN(f) ? null : f);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder put(Double d) {
|
||||
mJson.put(Double.isNaN(d) ? null : d);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder put(Boolean b) {
|
||||
mJson.put(b);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder put(SonarValue v) {
|
||||
return put(v.toSonarObject());
|
||||
}
|
||||
|
||||
public Builder put(SonarArray a) {
|
||||
mJson.put(a == null ? null : a.mJson);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder put(SonarArray.Builder b) {
|
||||
return put(b.build());
|
||||
}
|
||||
|
||||
public Builder put(SonarObject o) {
|
||||
mJson.put(o == null ? null : o.mJson);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder put(SonarObject.Builder b) {
|
||||
return put(b.build());
|
||||
}
|
||||
|
||||
public SonarArray build() {
|
||||
return new SonarArray(mJson);
|
||||
}
|
||||
}
|
||||
}
|
||||
20
android/core/SonarClient.java
Normal file
20
android/core/SonarClient.java
Normal file
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
package com.facebook.sonar.core;
|
||||
|
||||
public interface SonarClient {
|
||||
void addPlugin(SonarPlugin plugin);
|
||||
|
||||
<T extends SonarPlugin> T getPlugin(String id);
|
||||
|
||||
void removePlugin(SonarPlugin plugin);
|
||||
|
||||
void start();
|
||||
|
||||
void stop();
|
||||
}
|
||||
37
android/core/SonarConnection.java
Normal file
37
android/core/SonarConnection.java
Normal file
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
package com.facebook.sonar.core;
|
||||
|
||||
/**
|
||||
* A connection between a SonarPlugin and the desktop Sonar application. Register request handlers
|
||||
* to respond to calls made by the desktop application or directly send messages to the desktop
|
||||
* application.
|
||||
*/
|
||||
public interface SonarConnection {
|
||||
|
||||
/**
|
||||
* Call a remote method on the Sonar desktop application, passing an optional JSON object as a
|
||||
* parameter.
|
||||
*/
|
||||
void send(String method, SonarObject params);
|
||||
|
||||
/**
|
||||
* Call a remote method on the Sonar desktop application, passing an optional JSON array as a
|
||||
* parameter.
|
||||
*/
|
||||
void send(String method, SonarArray params);
|
||||
|
||||
/** Report client error */
|
||||
void reportError(Throwable throwable);
|
||||
|
||||
/**
|
||||
* Register a receiver for a remote method call issued by the Sonar desktop application. The
|
||||
* SonarReceiver is passed a responder to respond back to the desktop application.
|
||||
*/
|
||||
void receive(String method, SonarReceiver receiver);
|
||||
}
|
||||
124
android/core/SonarDynamic.java
Normal file
124
android/core/SonarDynamic.java
Normal file
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
package com.facebook.sonar.core;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
|
||||
public class SonarDynamic {
|
||||
private final Object mObject;
|
||||
|
||||
public SonarDynamic(Object object) {
|
||||
mObject = object;
|
||||
}
|
||||
|
||||
public @Nullable String asString() {
|
||||
if (mObject == null) {
|
||||
return null;
|
||||
}
|
||||
return mObject.toString();
|
||||
}
|
||||
|
||||
public int asInt() {
|
||||
if (mObject == null) {
|
||||
return 0;
|
||||
}
|
||||
if (mObject instanceof Integer) {
|
||||
return (Integer) mObject;
|
||||
}
|
||||
if (mObject instanceof Long) {
|
||||
return ((Long) mObject).intValue();
|
||||
}
|
||||
if (mObject instanceof Float) {
|
||||
return ((Float) mObject).intValue();
|
||||
}
|
||||
if (mObject instanceof Double) {
|
||||
return ((Double) mObject).intValue();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public long asLong() {
|
||||
if (mObject == null) {
|
||||
return 0;
|
||||
}
|
||||
if (mObject instanceof Integer) {
|
||||
return (Integer) mObject;
|
||||
}
|
||||
if (mObject instanceof Long) {
|
||||
return (Long) mObject;
|
||||
}
|
||||
if (mObject instanceof Float) {
|
||||
return ((Float) mObject).longValue();
|
||||
}
|
||||
if (mObject instanceof Double) {
|
||||
return ((Double) mObject).longValue();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public float asFloat() {
|
||||
if (mObject == null) {
|
||||
return 0;
|
||||
}
|
||||
if (mObject instanceof Integer) {
|
||||
return (Integer) mObject;
|
||||
}
|
||||
if (mObject instanceof Long) {
|
||||
return (Long) mObject;
|
||||
}
|
||||
if (mObject instanceof Float) {
|
||||
return (Float) mObject;
|
||||
}
|
||||
if (mObject instanceof Double) {
|
||||
return ((Double) mObject).floatValue();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public double asDouble() {
|
||||
if (mObject == null) {
|
||||
return 0;
|
||||
}
|
||||
if (mObject instanceof Integer) {
|
||||
return (Integer) mObject;
|
||||
}
|
||||
if (mObject instanceof Long) {
|
||||
return (Long) mObject;
|
||||
}
|
||||
if (mObject instanceof Float) {
|
||||
return (Float) mObject;
|
||||
}
|
||||
if (mObject instanceof Double) {
|
||||
return (Double) mObject;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public boolean asBoolean() {
|
||||
if (mObject == null) {
|
||||
return false;
|
||||
}
|
||||
return (Boolean) mObject;
|
||||
}
|
||||
|
||||
public SonarObject asObject() {
|
||||
if (mObject instanceof JSONObject) {
|
||||
return new SonarObject((JSONObject) mObject);
|
||||
}
|
||||
return (SonarObject) mObject;
|
||||
}
|
||||
|
||||
public SonarArray asArray() {
|
||||
if (mObject instanceof JSONArray) {
|
||||
return new SonarArray((JSONArray) mObject);
|
||||
}
|
||||
return (SonarArray) mObject;
|
||||
}
|
||||
}
|
||||
221
android/core/SonarObject.java
Normal file
221
android/core/SonarObject.java
Normal file
@@ -0,0 +1,221 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
package com.facebook.sonar.core;
|
||||
|
||||
import java.util.Arrays;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
public class SonarObject {
|
||||
final JSONObject mJson;
|
||||
|
||||
public SonarObject(JSONObject json) {
|
||||
mJson = (json != null ? json : new JSONObject());
|
||||
}
|
||||
|
||||
public SonarObject(String json) {
|
||||
try {
|
||||
mJson = new JSONObject(json);
|
||||
} catch (JSONException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public SonarDynamic getDynamic(String name) {
|
||||
return new SonarDynamic(mJson.opt(name));
|
||||
}
|
||||
|
||||
public String getString(String name) {
|
||||
if (mJson.isNull(name)) {
|
||||
return null;
|
||||
}
|
||||
return mJson.optString(name);
|
||||
}
|
||||
|
||||
public int getInt(String name) {
|
||||
return mJson.optInt(name);
|
||||
}
|
||||
|
||||
public long getLong(String name) {
|
||||
return mJson.optLong(name);
|
||||
}
|
||||
|
||||
public float getFloat(String name) {
|
||||
return (float) mJson.optDouble(name);
|
||||
}
|
||||
|
||||
public double getDouble(String name) {
|
||||
return mJson.optDouble(name);
|
||||
}
|
||||
|
||||
public boolean getBoolean(String name) {
|
||||
return mJson.optBoolean(name);
|
||||
}
|
||||
|
||||
public SonarObject getObject(String name) {
|
||||
final Object o = mJson.opt(name);
|
||||
return new SonarObject((JSONObject) o);
|
||||
}
|
||||
|
||||
public SonarArray getArray(String name) {
|
||||
final Object o = mJson.opt(name);
|
||||
return new SonarArray((JSONArray) o);
|
||||
}
|
||||
|
||||
public boolean contains(String name) {
|
||||
return mJson.has(name);
|
||||
}
|
||||
|
||||
public String toJsonString() {
|
||||
return toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return mJson.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
return mJson.toString().equals(o.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return mJson.hashCode();
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
private final JSONObject mJson;
|
||||
|
||||
public Builder() {
|
||||
mJson = new JSONObject();
|
||||
}
|
||||
|
||||
public Builder put(String name, Object obj) {
|
||||
if (obj == null) {
|
||||
return put(name, (String) null);
|
||||
} else if (obj instanceof Integer) {
|
||||
return put(name, (Integer) obj);
|
||||
} else if (obj instanceof Long) {
|
||||
return put(name, (Long) obj);
|
||||
} else if (obj instanceof Float) {
|
||||
return put(name, (Float) obj);
|
||||
} else if (obj instanceof Double) {
|
||||
return put(name, (Double) obj);
|
||||
} else if (obj instanceof String) {
|
||||
return put(name, (String) obj);
|
||||
} else if (obj instanceof Boolean) {
|
||||
return put(name, (Boolean) obj);
|
||||
} else if (obj instanceof Object[]) {
|
||||
return put(name, Arrays.deepToString((Object[]) obj));
|
||||
} else if (obj instanceof SonarObject) {
|
||||
return put(name, (SonarObject) obj);
|
||||
} else if (obj instanceof SonarObject.Builder) {
|
||||
return put(name, (SonarObject.Builder) obj);
|
||||
} else if (obj instanceof SonarArray) {
|
||||
return put(name, (SonarArray) obj);
|
||||
} else if (obj instanceof SonarArray.Builder) {
|
||||
return put(name, (SonarArray.Builder) obj);
|
||||
} else if (obj instanceof SonarValue) {
|
||||
return put(name, ((SonarValue) obj).toSonarObject());
|
||||
} else {
|
||||
return put(name, obj.toString());
|
||||
}
|
||||
}
|
||||
|
||||
public Builder put(String name, String s) {
|
||||
try {
|
||||
mJson.put(name, s);
|
||||
} catch (JSONException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder put(String name, Integer i) {
|
||||
try {
|
||||
mJson.put(name, i);
|
||||
} catch (JSONException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder put(String name, Long l) {
|
||||
try {
|
||||
mJson.put(name, l);
|
||||
} catch (JSONException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder put(String name, Float f) {
|
||||
try {
|
||||
mJson.put(name, Float.isNaN(f) ? null : f);
|
||||
} catch (JSONException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder put(String name, Double d) {
|
||||
try {
|
||||
mJson.put(name, Double.isNaN(d) ? null : d);
|
||||
} catch (JSONException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder put(String name, Boolean b) {
|
||||
try {
|
||||
mJson.put(name, b);
|
||||
} catch (JSONException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder put(String name, SonarValue v) {
|
||||
return put(name, v.toSonarObject());
|
||||
}
|
||||
|
||||
public Builder put(String name, SonarArray a) {
|
||||
try {
|
||||
mJson.put(name, a == null ? null : a.mJson);
|
||||
} catch (JSONException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder put(String name, SonarArray.Builder b) {
|
||||
return put(name, b.build());
|
||||
}
|
||||
|
||||
public Builder put(String name, SonarObject o) {
|
||||
try {
|
||||
mJson.put(name, o == null ? null : o.mJson);
|
||||
} catch (JSONException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder put(String name, SonarObject.Builder b) {
|
||||
return put(name, b.build());
|
||||
}
|
||||
|
||||
public SonarObject build() {
|
||||
return new SonarObject(mJson);
|
||||
}
|
||||
}
|
||||
}
|
||||
37
android/core/SonarPlugin.java
Normal file
37
android/core/SonarPlugin.java
Normal file
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
package com.facebook.sonar.core;
|
||||
|
||||
/**
|
||||
* A SonarPlugin is an object which exposes an API to the Desktop Sonar application. When a
|
||||
* connection is established the plugin is given a SonarConnection on which it can register request
|
||||
* handlers and send messages. When the SonarConnection is invalid onDisconnect is called. onConnect
|
||||
* may be called again on the same plugin object if Sonar re-connects, this will provide a new
|
||||
* SonarConnection, do not attempt to re-use the previous connection.
|
||||
*/
|
||||
public interface SonarPlugin {
|
||||
|
||||
/**
|
||||
* @return The id of this plugin. This is the namespace which Sonar desktop plugins will call
|
||||
* methods on to route them to your plugin. This should match the id specified in your React
|
||||
* plugin.
|
||||
*/
|
||||
String getId();
|
||||
|
||||
/**
|
||||
* Called when a connection has been established. The connection passed to this method is valid
|
||||
* until {@link SonarPlugin#onDisconnect()} is called.
|
||||
*/
|
||||
void onConnect(SonarConnection connection) throws Exception;
|
||||
|
||||
/**
|
||||
* Called when the connection passed to {@link SonarPlugin#onConnect(SonarConnection)} is no
|
||||
* longer valid. Do not try to use the connection in or after this method has been called.
|
||||
*/
|
||||
void onDisconnect() throws Exception;
|
||||
}
|
||||
25
android/core/SonarReceiver.java
Normal file
25
android/core/SonarReceiver.java
Normal file
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
package com.facebook.sonar.core;
|
||||
|
||||
/**
|
||||
* A receiver of a remote method call issued by the Sonar desktop application. If the given
|
||||
* responder is present it means the Sonar desktop application is expecting a response.
|
||||
*/
|
||||
public interface SonarReceiver {
|
||||
|
||||
/**
|
||||
* Reciver for a request sent from the Sonar desktop client.
|
||||
*
|
||||
* @param params Optional set of parameters sent with the request.
|
||||
* @param responder Optional responder for request. Some requests don't warrant a response
|
||||
* through. In this case the request should be made from the desktop using send() instead of
|
||||
* call().
|
||||
*/
|
||||
void onReceive(SonarObject params, SonarResponder responder) throws Exception;
|
||||
}
|
||||
28
android/core/SonarResponder.java
Normal file
28
android/core/SonarResponder.java
Normal file
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
package com.facebook.sonar.core;
|
||||
|
||||
/**
|
||||
* SonarResponder is used to asyncronously response to a messaged recieved from the Sonar desktop
|
||||
* app. The Sonar Responder will automatically wrap the response in an approriate object depending
|
||||
* on if it is an error or not.
|
||||
*/
|
||||
public interface SonarResponder {
|
||||
|
||||
/** Deliver a successful response to the Sonar desktop app. */
|
||||
void success(SonarObject response);
|
||||
|
||||
/** Deliver a successful response to the Sonar desktop app. */
|
||||
void success(SonarArray response);
|
||||
|
||||
/** Deliver a successful response to the Sonar desktop app. */
|
||||
void success();
|
||||
|
||||
/** Inform the Sonar desktop app of an error in handling the request. */
|
||||
void error(SonarObject response);
|
||||
}
|
||||
12
android/core/SonarValue.java
Normal file
12
android/core/SonarValue.java
Normal file
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
package com.facebook.sonar.core;
|
||||
|
||||
public interface SonarValue {
|
||||
SonarObject toSonarObject();
|
||||
}
|
||||
75
android/plugins/common/BufferingSonarPlugin.java
Normal file
75
android/plugins/common/BufferingSonarPlugin.java
Normal file
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
package com.facebook.sonar.plugins.common;
|
||||
|
||||
import com.facebook.sonar.core.SonarConnection;
|
||||
import com.facebook.sonar.core.SonarObject;
|
||||
import com.facebook.sonar.core.SonarPlugin;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Sonar plugin that keeps events in a buffer until a connection is available.
|
||||
*
|
||||
* <p>In order to send data to the {@link SonarConnection}, use {@link #send(String, SonarObject)}
|
||||
* instead of {@link SonarConnection#send(String, SonarObject)}.
|
||||
*/
|
||||
public abstract class BufferingSonarPlugin implements SonarPlugin {
|
||||
|
||||
private static final int BUFFER_SIZE = 500;
|
||||
|
||||
private @Nullable RingBuffer<CachedSonarEvent> mEventQueue;
|
||||
private @Nullable SonarConnection mConnection;
|
||||
|
||||
@Override
|
||||
public synchronized void onConnect(SonarConnection connection) {
|
||||
mConnection = connection;
|
||||
|
||||
sendBufferedEvents();
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void onDisconnect() {
|
||||
mConnection = null;
|
||||
}
|
||||
|
||||
public synchronized SonarConnection getConnection() {
|
||||
return mConnection;
|
||||
}
|
||||
|
||||
public synchronized boolean isConnected() {
|
||||
return mConnection != null;
|
||||
}
|
||||
|
||||
public synchronized void send(String method, SonarObject sonarObject) {
|
||||
if (mEventQueue == null) {
|
||||
mEventQueue = new RingBuffer<>(BUFFER_SIZE);
|
||||
}
|
||||
mEventQueue.enqueue(new CachedSonarEvent(method, sonarObject));
|
||||
if (mConnection != null) {
|
||||
mConnection.send(method, sonarObject);
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void sendBufferedEvents() {
|
||||
if (mEventQueue != null && mConnection != null) {
|
||||
for (CachedSonarEvent cachedSonarEvent : mEventQueue.asList()) {
|
||||
mConnection.send(cachedSonarEvent.method, cachedSonarEvent.sonarObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class CachedSonarEvent {
|
||||
final String method;
|
||||
final SonarObject sonarObject;
|
||||
|
||||
private CachedSonarEvent(String method, SonarObject sonarObject) {
|
||||
this.method = method;
|
||||
this.sonarObject = sonarObject;
|
||||
}
|
||||
}
|
||||
}
|
||||
40
android/plugins/common/MainThreadSonarReceiver.java
Normal file
40
android/plugins/common/MainThreadSonarReceiver.java
Normal file
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
package com.facebook.sonar.plugins.common;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import com.facebook.sonar.core.ErrorReportingRunnable;
|
||||
import com.facebook.sonar.core.SonarConnection;
|
||||
import com.facebook.sonar.core.SonarObject;
|
||||
import com.facebook.sonar.core.SonarReceiver;
|
||||
import com.facebook.sonar.core.SonarResponder;
|
||||
|
||||
public abstract class MainThreadSonarReceiver implements SonarReceiver {
|
||||
|
||||
public MainThreadSonarReceiver(SonarConnection connection) {
|
||||
this.mConnection = connection;
|
||||
}
|
||||
|
||||
private final SonarConnection mConnection;
|
||||
private final Handler mHandler = new Handler(Looper.getMainLooper());
|
||||
|
||||
@Override
|
||||
public final void onReceive(final SonarObject params, final SonarResponder responder) {
|
||||
mHandler.post(
|
||||
new ErrorReportingRunnable(mConnection) {
|
||||
@Override
|
||||
public void runOrThrow() throws Exception {
|
||||
onReceiveOnMainThread(params, responder);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public abstract void onReceiveOnMainThread(SonarObject params, SonarResponder responder)
|
||||
throws Exception;
|
||||
}
|
||||
31
android/plugins/common/RingBuffer.java
Normal file
31
android/plugins/common/RingBuffer.java
Normal file
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
package com.facebook.sonar.plugins.common;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
final class RingBuffer<T> {
|
||||
final int mBufferSize;
|
||||
final List<T> mBuffer = new LinkedList<>();
|
||||
|
||||
RingBuffer(int bufferSize) {
|
||||
mBufferSize = bufferSize;
|
||||
}
|
||||
|
||||
void enqueue(T item) {
|
||||
if (mBuffer.size() >= mBufferSize) {
|
||||
mBuffer.remove(0);
|
||||
}
|
||||
mBuffer.add(item);
|
||||
}
|
||||
|
||||
List<T> asList() {
|
||||
return mBuffer;
|
||||
}
|
||||
}
|
||||
35
android/plugins/console/ConsoleSonarPlugin.java
Normal file
35
android/plugins/console/ConsoleSonarPlugin.java
Normal file
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
package com.facebook.sonar.plugins.console;
|
||||
|
||||
import com.facebook.sonar.core.SonarConnection;
|
||||
import com.facebook.sonar.core.SonarPlugin;
|
||||
import com.facebook.sonar.plugins.console.iface.ConsoleCommandReceiver;
|
||||
|
||||
public class ConsoleSonarPlugin implements SonarPlugin {
|
||||
|
||||
private final JavascriptEnvironment mJavascriptEnvironment;
|
||||
private JavascriptSession mJavascriptSession;
|
||||
|
||||
public ConsoleSonarPlugin(JavascriptEnvironment jsEnvironment) {
|
||||
this.mJavascriptEnvironment = jsEnvironment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "Console";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnect(SonarConnection connection) throws Exception {
|
||||
ConsoleCommandReceiver.listenForCommands(connection, mJavascriptEnvironment);
|
||||
}
|
||||
|
||||
public void onDisconnect() throws Exception {
|
||||
}
|
||||
}
|
||||
58
android/plugins/console/JavascriptEnvironment.java
Normal file
58
android/plugins/console/JavascriptEnvironment.java
Normal file
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
package com.facebook.sonar.plugins.console;
|
||||
|
||||
import com.facebook.sonar.plugins.console.iface.ScriptingEnvironment;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import org.mozilla.javascript.Context;
|
||||
import org.mozilla.javascript.ContextFactory;
|
||||
|
||||
public class JavascriptEnvironment implements ScriptingEnvironment {
|
||||
|
||||
private final Map<String, Object> mBoundVariables;
|
||||
private final ContextFactory mContextFactory;
|
||||
|
||||
public JavascriptEnvironment() {
|
||||
mBoundVariables = new HashMap<>();
|
||||
mContextFactory =
|
||||
new ContextFactory() {
|
||||
@Override
|
||||
public boolean hasFeature(Context cx, int featureIndex) {
|
||||
return featureIndex == Context.FEATURE_ENHANCED_JAVA_ACCESS;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public JavascriptSession startSession() {
|
||||
return new JavascriptSession(mContextFactory, mBoundVariables);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method for other plugins to register objects to a name, so that they can be accessed in all
|
||||
* console sessions.
|
||||
*
|
||||
* @param name The variable name to bind the object to.
|
||||
* @param object The reference to bind.
|
||||
*/
|
||||
@Override
|
||||
public void registerGlobalObject(String name, Object object) {
|
||||
if (mBoundVariables.containsKey(name)) {
|
||||
throw new IllegalStateException(
|
||||
String.format(
|
||||
"Variable %s is already reserved for %s", name, mBoundVariables.get(name)));
|
||||
}
|
||||
mBoundVariables.put(name, object);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
169
android/plugins/console/JavascriptSession.java
Normal file
169
android/plugins/console/JavascriptSession.java
Normal file
@@ -0,0 +1,169 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
package com.facebook.sonar.plugins.console;
|
||||
|
||||
import com.facebook.sonar.plugins.console.iface.ScriptingSession;
|
||||
import java.io.Closeable;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.json.JSONTokener;
|
||||
import org.mozilla.javascript.Context;
|
||||
import org.mozilla.javascript.ContextFactory;
|
||||
import org.mozilla.javascript.Function;
|
||||
import org.mozilla.javascript.NativeJSON;
|
||||
import org.mozilla.javascript.NativeJavaMethod;
|
||||
import org.mozilla.javascript.NativeJavaObject;
|
||||
import org.mozilla.javascript.Scriptable;
|
||||
import org.mozilla.javascript.ScriptableObject;
|
||||
import org.mozilla.javascript.Undefined;
|
||||
|
||||
public class JavascriptSession implements Closeable, ScriptingSession {
|
||||
|
||||
private static final String TYPE = "type";
|
||||
private static final String VALUE = "value";
|
||||
public static final String JSON = "json";
|
||||
private final Context mContext;
|
||||
private final ContextFactory mContextFactory;
|
||||
private final Scriptable mScope;
|
||||
private final AtomicInteger lineNumber = new AtomicInteger(0);
|
||||
|
||||
JavascriptSession(ContextFactory contextFactory, Map<String, Object> globals) {
|
||||
mContextFactory = contextFactory;
|
||||
mContext = contextFactory.enterContext();
|
||||
|
||||
// Interpreted mode, or it will produce Dalvik incompatible bytecode.
|
||||
mContext.setOptimizationLevel(-1);
|
||||
mScope = mContext.initStandardObjects();
|
||||
|
||||
for (Map.Entry<String, Object> entry : globals.entrySet()) {
|
||||
final Object value = entry.getValue();
|
||||
|
||||
if (value instanceof Number || value instanceof String) {
|
||||
ScriptableObject.putConstProperty(mScope, entry.getKey(), entry.getValue());
|
||||
} else {
|
||||
// Calling java methods in the VM produces objects wrapped in NativeJava*.
|
||||
// So passing in wrapped objects keeps them consistent.
|
||||
ScriptableObject.putConstProperty(
|
||||
mScope,
|
||||
entry.getKey(),
|
||||
new NativeJavaObject(mScope, entry.getValue(), entry.getValue().getClass()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public JSONObject evaluateCommand(String userScript) throws JSONException {
|
||||
return evaluateCommand(userScript, mScope);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JSONObject evaluateCommand(String userScript, Object context) throws JSONException {
|
||||
Scriptable scope = new NativeJavaObject(mScope, context, context.getClass());
|
||||
return evaluateCommand(userScript, scope);
|
||||
}
|
||||
|
||||
private JSONObject evaluateCommand(String command, Scriptable scope) throws JSONException {
|
||||
try {
|
||||
// This may be called by any thread, and contexts have to be entered in the current thread
|
||||
// before being used, so enter/exit every time.
|
||||
mContextFactory.enterContext();
|
||||
return toJson(
|
||||
mContext.evaluateString(
|
||||
scope, command, "sonar-console", lineNumber.incrementAndGet(), null));
|
||||
} finally {
|
||||
Context.exit();
|
||||
}
|
||||
}
|
||||
|
||||
private JSONObject toJson(Object result) throws JSONException {
|
||||
|
||||
if (result instanceof String) {
|
||||
return new JSONObject().put(TYPE, JSON).put(VALUE, result);
|
||||
}
|
||||
|
||||
if (result instanceof Class) {
|
||||
return new JSONObject().put(TYPE, "class").put(VALUE, ((Class) result).getName());
|
||||
}
|
||||
|
||||
if (result instanceof NativeJavaObject
|
||||
&& ((NativeJavaObject) result).unwrap() instanceof String) {
|
||||
return new JSONObject().put(TYPE, JSON).put(VALUE, ((NativeJavaObject) result).unwrap());
|
||||
}
|
||||
|
||||
if (result instanceof NativeJavaObject
|
||||
&& ((NativeJavaObject) result).unwrap() instanceof Class) {
|
||||
return new JSONObject()
|
||||
.put(TYPE, "class")
|
||||
.put(VALUE, ((NativeJavaObject) result).unwrap().toString());
|
||||
}
|
||||
|
||||
if (result instanceof NativeJavaObject) {
|
||||
final JSONObject o = new JSONObject();
|
||||
o.put("toString", ((NativeJavaObject) result).unwrap().toString());
|
||||
for (Object id : ((NativeJavaObject) result).getIds()) {
|
||||
if (id instanceof String) {
|
||||
final String name = (String) id;
|
||||
final Object value = ((NativeJavaObject) result).get(name, (NativeJavaObject) result);
|
||||
if (value != null && value instanceof NativeJavaMethod) {
|
||||
continue;
|
||||
}
|
||||
final String valueString = value == null ? null : safeUnwrap(value).toString();
|
||||
o.put(name, valueString);
|
||||
}
|
||||
}
|
||||
return new JSONObject().put(TYPE, "javaObject").put(VALUE, o);
|
||||
}
|
||||
|
||||
if (result instanceof NativeJavaMethod) {
|
||||
final JSONObject o = new JSONObject();
|
||||
o.put(TYPE, "method");
|
||||
o.put("name", ((NativeJavaMethod) result).getFunctionName());
|
||||
return o;
|
||||
}
|
||||
|
||||
if (result == null || result instanceof Undefined) {
|
||||
return new JSONObject().put(TYPE, "null");
|
||||
}
|
||||
|
||||
if (result instanceof Function) {
|
||||
final JSONObject o = new JSONObject();
|
||||
o.put(TYPE, "function");
|
||||
o.put(VALUE, Context.toString(result));
|
||||
return o;
|
||||
}
|
||||
|
||||
if (result instanceof ScriptableObject) {
|
||||
return new JSONObject()
|
||||
.put(TYPE, JSON)
|
||||
.put(
|
||||
VALUE,
|
||||
new JSONTokener(NativeJSON.stringify(mContext, mScope, result, null, null).toString())
|
||||
.nextValue());
|
||||
}
|
||||
|
||||
if (result instanceof Number) {
|
||||
return new JSONObject().put(TYPE, JSON).put(VALUE, result);
|
||||
}
|
||||
|
||||
return new JSONObject().put(TYPE, "unknown").put(VALUE, result.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
Context.exit();
|
||||
}
|
||||
|
||||
private static Object safeUnwrap(Object o) {
|
||||
if (o instanceof NativeJavaObject) {
|
||||
return ((NativeJavaObject) o).unwrap();
|
||||
}
|
||||
return o;
|
||||
}
|
||||
}
|
||||
88
android/plugins/console/iface/ConsoleCommandReceiver.java
Normal file
88
android/plugins/console/iface/ConsoleCommandReceiver.java
Normal file
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
package com.facebook.sonar.plugins.console.iface;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
import com.facebook.sonar.core.SonarConnection;
|
||||
import com.facebook.sonar.core.SonarObject;
|
||||
import com.facebook.sonar.core.SonarReceiver;
|
||||
import com.facebook.sonar.core.SonarResponder;
|
||||
import com.facebook.sonar.plugins.common.MainThreadSonarReceiver;
|
||||
import org.json.JSONObject;
|
||||
|
||||
/**
|
||||
* Convenience class for adding console execution to a Sonar Plugin. Calling {@link
|
||||
* ConsoleCommandReceiver#listenForCommands(SonarConnection, ScriptingEnvironment, ContextProvider)}
|
||||
* will add the necessary listeners for responding to command execution calls.
|
||||
*/
|
||||
public class ConsoleCommandReceiver {
|
||||
|
||||
/**
|
||||
* Incoming command execution calls may reference a context ID that means something to your
|
||||
* plugin. Implement {@link ContextProvider} to provide a mapping from context ID to java object.
|
||||
* This will allow your sonar plugin to control the execution context of the command.
|
||||
*/
|
||||
public interface ContextProvider {
|
||||
@Nullable
|
||||
Object getObjectForId(String id);
|
||||
}
|
||||
|
||||
public static void listenForCommands(
|
||||
final SonarConnection connection,
|
||||
final ScriptingEnvironment scriptingEnvironment,
|
||||
final ContextProvider contextProvider) {
|
||||
|
||||
final ScriptingSession session = scriptingEnvironment.startSession();
|
||||
final SonarReceiver executeCommandReceiver =
|
||||
new MainThreadSonarReceiver(connection) {
|
||||
@Override
|
||||
public void onReceiveOnMainThread(SonarObject params, SonarResponder responder)
|
||||
throws Exception {
|
||||
final String command = params.getString("command");
|
||||
final String contextObjectId = params.getString("context");
|
||||
final Object contextObject = contextProvider.getObjectForId(contextObjectId);
|
||||
try {
|
||||
JSONObject o =
|
||||
contextObject == null
|
||||
? session.evaluateCommand(command)
|
||||
: session.evaluateCommand(command, contextObject);
|
||||
responder.success(new SonarObject(o));
|
||||
} catch (Exception e) {
|
||||
responder.error(new SonarObject.Builder().put("message", e.getMessage()).build());
|
||||
}
|
||||
}
|
||||
};
|
||||
final SonarReceiver isEnabledReceiver =
|
||||
new SonarReceiver() {
|
||||
@Override
|
||||
public void onReceive(SonarObject params, SonarResponder responder) throws Exception {
|
||||
responder.success(
|
||||
new SonarObject.Builder()
|
||||
.put("isEnabled", scriptingEnvironment.isEnabled())
|
||||
.build());
|
||||
}
|
||||
};
|
||||
|
||||
connection.receive("executeCommand", executeCommandReceiver);
|
||||
connection.receive("isConsoleEnabled", isEnabledReceiver);
|
||||
}
|
||||
|
||||
public static void listenForCommands(
|
||||
SonarConnection connection, ScriptingEnvironment scriptingEnvironment) {
|
||||
listenForCommands(connection, scriptingEnvironment, nullContextProvider);
|
||||
}
|
||||
|
||||
private static final ContextProvider nullContextProvider =
|
||||
new ContextProvider() {
|
||||
@Override
|
||||
@Nullable
|
||||
public Object getObjectForId(String id) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
43
android/plugins/console/iface/NullScriptingEnvironment.java
Normal file
43
android/plugins/console/iface/NullScriptingEnvironment.java
Normal file
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
package com.facebook.sonar.plugins.console.iface;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
public class NullScriptingEnvironment implements ScriptingEnvironment {
|
||||
|
||||
@Override
|
||||
public ScriptingSession startSession() {
|
||||
return new NoOpScriptingSession();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerGlobalObject(String name, Object object) {}
|
||||
|
||||
static class NoOpScriptingSession implements ScriptingSession {
|
||||
|
||||
@Override
|
||||
public JSONObject evaluateCommand(String userScript) throws JSONException {
|
||||
throw new UnsupportedOperationException("Console plugin not enabled in this app");
|
||||
}
|
||||
|
||||
@Override
|
||||
public JSONObject evaluateCommand(String userScript, Object context) throws JSONException {
|
||||
return evaluateCommand(userScript);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
17
android/plugins/console/iface/ScriptingEnvironment.java
Normal file
17
android/plugins/console/iface/ScriptingEnvironment.java
Normal file
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
package com.facebook.sonar.plugins.console.iface;
|
||||
|
||||
public interface ScriptingEnvironment {
|
||||
|
||||
ScriptingSession startSession();
|
||||
|
||||
void registerGlobalObject(String name, Object object);
|
||||
|
||||
boolean isEnabled();
|
||||
}
|
||||
20
android/plugins/console/iface/ScriptingSession.java
Normal file
20
android/plugins/console/iface/ScriptingSession.java
Normal file
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
package com.facebook.sonar.plugins.console.iface;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
public interface ScriptingSession {
|
||||
|
||||
JSONObject evaluateCommand(String userScript) throws JSONException;
|
||||
|
||||
JSONObject evaluateCommand(String userScript, Object context) throws JSONException;
|
||||
|
||||
void close();
|
||||
}
|
||||
128
android/plugins/inspector/ApplicationWrapper.java
Normal file
128
android/plugins/inspector/ApplicationWrapper.java
Normal file
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.facebook.sonar.plugins.inspector;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Application;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.view.View;
|
||||
import com.facebook.sonar.plugins.inspector.descriptors.utils.AndroidRootResolver;
|
||||
import com.facebook.sonar.plugins.inspector.descriptors.utils.AndroidRootResolver.Root;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
public class ApplicationWrapper implements Application.ActivityLifecycleCallbacks {
|
||||
|
||||
public interface ActivityStackChangedListener {
|
||||
void onActivityStackChanged(List<Activity> stack);
|
||||
}
|
||||
|
||||
private final Application mApplication;
|
||||
private final AndroidRootResolver mAndroidRootsResolver;
|
||||
private final List<WeakReference<Activity>> mActivities;
|
||||
private final Handler mHandler;
|
||||
private ActivityStackChangedListener mListener;
|
||||
|
||||
public ApplicationWrapper(Application application) {
|
||||
mApplication = application;
|
||||
mAndroidRootsResolver = new AndroidRootResolver();
|
||||
mApplication.registerActivityLifecycleCallbacks(this);
|
||||
mActivities = new ArrayList<>();
|
||||
mHandler = new Handler(Looper.getMainLooper());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
|
||||
mActivities.add(new WeakReference<>(activity));
|
||||
notifyListener();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityStarted(Activity activity) {}
|
||||
|
||||
@Override
|
||||
public void onActivityResumed(Activity activity) {}
|
||||
|
||||
@Override
|
||||
public void onActivityPaused(Activity activity) {
|
||||
if (activity.isFinishing()) {
|
||||
final Iterator<WeakReference<Activity>> activityIterator = mActivities.iterator();
|
||||
|
||||
while (activityIterator.hasNext()) {
|
||||
if (activityIterator.next().get() == activity) {
|
||||
activityIterator.remove();
|
||||
}
|
||||
}
|
||||
|
||||
notifyListener();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityStopped(Activity activity) {}
|
||||
|
||||
@Override
|
||||
public void onActivitySaveInstanceState(Activity activity, Bundle outState) {}
|
||||
|
||||
@Override
|
||||
public void onActivityDestroyed(Activity activity) {}
|
||||
|
||||
private void notifyListener() {
|
||||
if (mListener != null) {
|
||||
mListener.onActivityStackChanged(getActivityStack());
|
||||
}
|
||||
}
|
||||
|
||||
public void setListener(ActivityStackChangedListener listener) {
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
public Application getApplication() {
|
||||
return mApplication;
|
||||
}
|
||||
|
||||
public List<Activity> getActivityStack() {
|
||||
final List<Activity> activities = new ArrayList<>(mActivities.size());
|
||||
final Iterator<WeakReference<Activity>> activityIterator = mActivities.iterator();
|
||||
|
||||
while (activityIterator.hasNext()) {
|
||||
final Activity activity = activityIterator.next().get();
|
||||
if (activity == null) {
|
||||
activityIterator.remove();
|
||||
} else {
|
||||
activities.add(activity);
|
||||
}
|
||||
}
|
||||
|
||||
return activities;
|
||||
}
|
||||
|
||||
public List<View> getViewRoots() {
|
||||
final List<Root> roots = mAndroidRootsResolver.listActiveRoots();
|
||||
if (roots == null) {
|
||||
return Collections.EMPTY_LIST;
|
||||
}
|
||||
|
||||
final List<View> viewRoots = new ArrayList<>(roots.size());
|
||||
for (Root root : roots) {
|
||||
viewRoots.add(root.view);
|
||||
}
|
||||
|
||||
return viewRoots;
|
||||
}
|
||||
|
||||
public void postDelayed(Runnable r, long delayMillis) {
|
||||
mHandler.postDelayed(r, delayMillis);
|
||||
}
|
||||
}
|
||||
175
android/plugins/inspector/BoundsDrawable.java
Normal file
175
android/plugins/inspector/BoundsDrawable.java
Normal file
@@ -0,0 +1,175 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.facebook.sonar.plugins.inspector;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.ColorFilter;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.Region;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.TextPaint;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class BoundsDrawable extends Drawable {
|
||||
public static final int COLOR_HIGHLIGHT_CONTENT = 0x888875c5;
|
||||
public static final int COLOR_HIGHLIGHT_PADDING = 0x889dd185;
|
||||
public static final int COLOR_HIGHLIGHT_MARGIN = 0x88f7b77b;
|
||||
private static @Nullable BoundsDrawable sInstance;
|
||||
|
||||
private final TextPaint mTextPaint;
|
||||
private final Paint mMarginPaint;
|
||||
private final Paint mPaddingPaint;
|
||||
private final Paint mContentPaint;
|
||||
private final Rect mWorkRect;
|
||||
private final Rect mMarginBounds;
|
||||
private final Rect mPaddingBounds;
|
||||
private final Rect mContentBounds;
|
||||
|
||||
private final int mStrokeWidth;
|
||||
private final float mAscentOffset;
|
||||
private final float mDensity;
|
||||
|
||||
public static BoundsDrawable getInstance(
|
||||
float density, Rect marginBounds, Rect paddingBounds, Rect contentBounds) {
|
||||
final BoundsDrawable drawable = getInstance(density);
|
||||
drawable.setBounds(marginBounds, paddingBounds, contentBounds);
|
||||
return drawable;
|
||||
}
|
||||
|
||||
public static BoundsDrawable getInstance(float density) {
|
||||
if (sInstance == null) {
|
||||
sInstance = new BoundsDrawable(density);
|
||||
}
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
private BoundsDrawable(float density) {
|
||||
mWorkRect = new Rect();
|
||||
mMarginBounds = new Rect();
|
||||
mPaddingBounds = new Rect();
|
||||
mContentBounds = new Rect();
|
||||
|
||||
mDensity = density;
|
||||
|
||||
mTextPaint = new TextPaint();
|
||||
mTextPaint.setAntiAlias(true);
|
||||
mTextPaint.setTextAlign(Paint.Align.CENTER);
|
||||
mTextPaint.setTextSize(dpToPx(8f));
|
||||
mAscentOffset = -mTextPaint.ascent() / 2f;
|
||||
mStrokeWidth = dpToPx(2f);
|
||||
|
||||
mPaddingPaint = new Paint();
|
||||
mPaddingPaint.setStyle(Paint.Style.FILL);
|
||||
mPaddingPaint.setColor(COLOR_HIGHLIGHT_PADDING);
|
||||
|
||||
mContentPaint = new Paint();
|
||||
mContentPaint.setStyle(Paint.Style.FILL);
|
||||
mContentPaint.setColor(COLOR_HIGHLIGHT_CONTENT);
|
||||
|
||||
mMarginPaint = new Paint();
|
||||
mMarginPaint.setStyle(Paint.Style.FILL);
|
||||
mMarginPaint.setColor(COLOR_HIGHLIGHT_MARGIN);
|
||||
}
|
||||
|
||||
public void setBounds(Rect marginBounds, Rect paddingBounds, Rect contentBounds) {
|
||||
mMarginBounds.set(marginBounds);
|
||||
mPaddingBounds.set(paddingBounds);
|
||||
mContentBounds.set(contentBounds);
|
||||
setBounds(marginBounds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Canvas canvas) {
|
||||
canvas.drawRect(mContentBounds, mContentPaint);
|
||||
|
||||
int saveCount = canvas.save();
|
||||
canvas.clipRect(mContentBounds, Region.Op.DIFFERENCE);
|
||||
canvas.drawRect(mPaddingBounds, mPaddingPaint);
|
||||
canvas.restoreToCount(saveCount);
|
||||
|
||||
saveCount = canvas.save();
|
||||
canvas.clipRect(mPaddingBounds, Region.Op.DIFFERENCE);
|
||||
canvas.drawRect(mMarginBounds, mMarginPaint);
|
||||
canvas.restoreToCount(saveCount);
|
||||
|
||||
drawBoundsDimensions(canvas, mContentBounds);
|
||||
|
||||
// Disabled for now since Sonar doesn't support options too well at this point in time.
|
||||
// Once options are supported, we should re-enable the calls below
|
||||
// drawCardinalDimensionsBetween(canvas, mContentBounds, mPaddingBounds);
|
||||
// drawCardinalDimensionsBetween(canvas, mPaddingBounds, mMarginBounds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAlpha(int alpha) {
|
||||
// No-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColorFilter(ColorFilter colorFilter) {
|
||||
// No-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOpacity() {
|
||||
return PixelFormat.TRANSLUCENT;
|
||||
}
|
||||
|
||||
private void drawCardinalDimensionsBetween(Canvas canvas, Rect inner, Rect outer) {
|
||||
mWorkRect.set(inner);
|
||||
mWorkRect.left = outer.left;
|
||||
mWorkRect.right = inner.left;
|
||||
drawBoundsDimension(canvas, mWorkRect, mWorkRect.width());
|
||||
mWorkRect.left = inner.right;
|
||||
mWorkRect.right = outer.right;
|
||||
drawBoundsDimension(canvas, mWorkRect, mWorkRect.width());
|
||||
mWorkRect.set(outer);
|
||||
mWorkRect.bottom = inner.top;
|
||||
drawBoundsDimension(canvas, mWorkRect, mWorkRect.height());
|
||||
mWorkRect.bottom = outer.bottom;
|
||||
mWorkRect.top = inner.bottom;
|
||||
drawBoundsDimension(canvas, mWorkRect, mWorkRect.height());
|
||||
}
|
||||
|
||||
private void drawBoundsDimension(Canvas canvas, Rect bounds, int value) {
|
||||
if (value <= 0) {
|
||||
return;
|
||||
}
|
||||
int saveCount = canvas.save();
|
||||
canvas.translate(bounds.centerX(), bounds.centerY());
|
||||
drawOutlinedText(canvas, value + "px");
|
||||
canvas.restoreToCount(saveCount);
|
||||
}
|
||||
|
||||
private void drawBoundsDimensions(Canvas canvas, Rect bounds) {
|
||||
int saveCount = canvas.save();
|
||||
canvas.translate(bounds.centerX(), bounds.centerY());
|
||||
drawOutlinedText(canvas, bounds.width() + "px \u00D7 " + bounds.height() + "px");
|
||||
canvas.restoreToCount(saveCount);
|
||||
}
|
||||
|
||||
private void drawOutlinedText(Canvas canvas, String text) {
|
||||
mTextPaint.setColor(Color.BLACK);
|
||||
mTextPaint.setStrokeWidth(mStrokeWidth);
|
||||
mTextPaint.setStyle(Paint.Style.STROKE);
|
||||
canvas.drawText(text, 0f, mAscentOffset, mTextPaint);
|
||||
|
||||
mTextPaint.setColor(Color.WHITE);
|
||||
mTextPaint.setStrokeWidth(0f);
|
||||
mTextPaint.setStyle(Paint.Style.FILL);
|
||||
canvas.drawText(text, 0f, mAscentOffset, mTextPaint);
|
||||
}
|
||||
|
||||
private int dpToPx(float dp) {
|
||||
return (int) (dp * mDensity);
|
||||
}
|
||||
}
|
||||
89
android/plugins/inspector/DescriptorMapping.java
Normal file
89
android/plugins/inspector/DescriptorMapping.java
Normal file
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.facebook.sonar.plugins.inspector;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Dialog;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.Window;
|
||||
import android.widget.TextView;
|
||||
import com.facebook.sonar.core.SonarConnection;
|
||||
import com.facebook.sonar.plugins.inspector.descriptors.ActivityDescriptor;
|
||||
import com.facebook.sonar.plugins.inspector.descriptors.ApplicationDescriptor;
|
||||
import com.facebook.sonar.plugins.inspector.descriptors.DialogDescriptor;
|
||||
import com.facebook.sonar.plugins.inspector.descriptors.DialogFragmentDescriptor;
|
||||
import com.facebook.sonar.plugins.inspector.descriptors.DrawableDescriptor;
|
||||
import com.facebook.sonar.plugins.inspector.descriptors.FragmentDescriptor;
|
||||
import com.facebook.sonar.plugins.inspector.descriptors.ObjectDescriptor;
|
||||
import com.facebook.sonar.plugins.inspector.descriptors.SupportDialogFragmentDescriptor;
|
||||
import com.facebook.sonar.plugins.inspector.descriptors.SupportFragmentDescriptor;
|
||||
import com.facebook.sonar.plugins.inspector.descriptors.TextViewDescriptor;
|
||||
import com.facebook.sonar.plugins.inspector.descriptors.ViewDescriptor;
|
||||
import com.facebook.sonar.plugins.inspector.descriptors.ViewGroupDescriptor;
|
||||
import com.facebook.sonar.plugins.inspector.descriptors.WindowDescriptor;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A mapping from classes to the object use to describe instances of a class. When looking for a
|
||||
* descriptor to describe an object this classs will traverse the object's class hierarchy until it
|
||||
* finds a matching descriptor instance.
|
||||
*/
|
||||
public class DescriptorMapping {
|
||||
private Map<Class<?>, NodeDescriptor<?>> mMapping = new HashMap<>();
|
||||
|
||||
/**
|
||||
* @return A DescriptorMapping initialized with default descriptors for java and Android classes.
|
||||
*/
|
||||
public static DescriptorMapping withDefaults() {
|
||||
final DescriptorMapping mapping = new DescriptorMapping();
|
||||
mapping.register(Object.class, new ObjectDescriptor());
|
||||
mapping.register(ApplicationWrapper.class, new ApplicationDescriptor());
|
||||
mapping.register(Activity.class, new ActivityDescriptor());
|
||||
mapping.register(Window.class, new WindowDescriptor());
|
||||
mapping.register(ViewGroup.class, new ViewGroupDescriptor());
|
||||
mapping.register(View.class, new ViewDescriptor());
|
||||
mapping.register(TextView.class, new TextViewDescriptor());
|
||||
mapping.register(Drawable.class, new DrawableDescriptor());
|
||||
mapping.register(Dialog.class, new DialogDescriptor());
|
||||
mapping.register(android.app.Fragment.class, new FragmentDescriptor());
|
||||
mapping.register(android.support.v4.app.Fragment.class, new SupportFragmentDescriptor());
|
||||
mapping.register(android.app.DialogFragment.class, new DialogFragmentDescriptor());
|
||||
mapping.register(
|
||||
android.support.v4.app.DialogFragment.class, new SupportDialogFragmentDescriptor());
|
||||
return mapping;
|
||||
}
|
||||
|
||||
/** Register a descriptor for a given class. */
|
||||
public <T> void register(Class<T> clazz, NodeDescriptor<T> descriptor) {
|
||||
mMapping.put(clazz, descriptor);
|
||||
}
|
||||
|
||||
NodeDescriptor<?> descriptorForClass(Class<?> clazz) {
|
||||
while (!mMapping.containsKey(clazz)) {
|
||||
clazz = clazz.getSuperclass();
|
||||
}
|
||||
return mMapping.get(clazz);
|
||||
}
|
||||
|
||||
void onConnect(SonarConnection connection) {
|
||||
for (NodeDescriptor descriptor : mMapping.values()) {
|
||||
descriptor.setConnection(connection);
|
||||
descriptor.setDescriptorMapping(this);
|
||||
}
|
||||
}
|
||||
|
||||
void onDisconnect() {
|
||||
for (NodeDescriptor descriptor : mMapping.values()) {
|
||||
descriptor.setConnection(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
12
android/plugins/inspector/HiddenNode.java
Normal file
12
android/plugins/inspector/HiddenNode.java
Normal file
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.facebook.sonar.plugins.inspector;
|
||||
|
||||
/** Marker interface to identify nodes which should not be traversed. */
|
||||
public interface HiddenNode {}
|
||||
69
android/plugins/inspector/HighlightedOverlay.java
Normal file
69
android/plugins/inspector/HighlightedOverlay.java
Normal file
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.facebook.sonar.plugins.inspector;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.view.View;
|
||||
|
||||
/**
|
||||
* A singleton instance of a overlay drawable used for highlighting node bounds. See {@link
|
||||
* NodeDescriptor#setHighlighted(Object, boolean)}.
|
||||
*/
|
||||
public class HighlightedOverlay {
|
||||
private static final boolean VIEW_OVERLAY_SUPPORT =
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2;
|
||||
|
||||
/**
|
||||
* Highlights a particular view with its content bounds, padding and margin dimensions
|
||||
*
|
||||
* @param targetView The view to apply the highlight on
|
||||
* @param margin A {@link Rect} containing the margin values
|
||||
* @param padding A {@link Rect} containing the padding values
|
||||
* @param contentBounds The {@link Rect} bounds of the content, which includes padding
|
||||
*/
|
||||
public static void setHighlighted(
|
||||
View targetView, Rect margin, Rect padding, Rect contentBounds) {
|
||||
if (!VIEW_OVERLAY_SUPPORT) {
|
||||
return;
|
||||
}
|
||||
|
||||
contentBounds.set(
|
||||
contentBounds.left + padding.left,
|
||||
contentBounds.top + padding.top,
|
||||
contentBounds.right - padding.right,
|
||||
contentBounds.bottom - padding.bottom);
|
||||
|
||||
padding = enclose(padding, contentBounds);
|
||||
margin = enclose(margin, padding);
|
||||
|
||||
final float density = targetView.getContext().getResources().getDisplayMetrics().density;
|
||||
final Drawable overlay = BoundsDrawable.getInstance(density, margin, padding, contentBounds);
|
||||
targetView.getOverlay().add(overlay);
|
||||
}
|
||||
|
||||
public static void removeHighlight(View targetView) {
|
||||
if (!VIEW_OVERLAY_SUPPORT) {
|
||||
return;
|
||||
}
|
||||
|
||||
final float density = targetView.getContext().getResources().getDisplayMetrics().density;
|
||||
final Drawable overlay = BoundsDrawable.getInstance(density);
|
||||
targetView.getOverlay().remove(overlay);
|
||||
}
|
||||
|
||||
private static Rect enclose(Rect parent, Rect child) {
|
||||
return new Rect(
|
||||
child.left - parent.left,
|
||||
child.top - parent.top,
|
||||
child.right + parent.right,
|
||||
child.bottom + parent.bottom);
|
||||
}
|
||||
}
|
||||
418
android/plugins/inspector/InspectorSonarPlugin.java
Normal file
418
android/plugins/inspector/InspectorSonarPlugin.java
Normal file
@@ -0,0 +1,418 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.facebook.sonar.plugins.inspector;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import com.facebook.sonar.core.ErrorReportingRunnable;
|
||||
import com.facebook.sonar.core.SonarArray;
|
||||
import com.facebook.sonar.core.SonarConnection;
|
||||
import com.facebook.sonar.core.SonarDynamic;
|
||||
import com.facebook.sonar.core.SonarObject;
|
||||
import com.facebook.sonar.core.SonarPlugin;
|
||||
import com.facebook.sonar.core.SonarReceiver;
|
||||
import com.facebook.sonar.core.SonarResponder;
|
||||
import com.facebook.sonar.plugins.common.MainThreadSonarReceiver;
|
||||
import com.facebook.sonar.plugins.console.iface.ConsoleCommandReceiver;
|
||||
import com.facebook.sonar.plugins.console.iface.NullScriptingEnvironment;
|
||||
import com.facebook.sonar.plugins.console.iface.ScriptingEnvironment;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class InspectorSonarPlugin implements SonarPlugin {
|
||||
|
||||
private ApplicationWrapper mApplication;
|
||||
private DescriptorMapping mDescriptorMapping;
|
||||
private ObjectTracker mObjectTracker;
|
||||
private ScriptingEnvironment mScriptingEnvironment;
|
||||
private String mHighlightedId;
|
||||
private TouchOverlayView mTouchOverlay;
|
||||
private SonarConnection mConnection;
|
||||
|
||||
public InspectorSonarPlugin(
|
||||
Context context,
|
||||
DescriptorMapping descriptorMapping,
|
||||
ScriptingEnvironment scriptingEnvironment) {
|
||||
this(
|
||||
new ApplicationWrapper((Application) context.getApplicationContext()),
|
||||
descriptorMapping,
|
||||
scriptingEnvironment);
|
||||
}
|
||||
|
||||
public InspectorSonarPlugin(Context context, DescriptorMapping descriptorMapping) {
|
||||
this(context, descriptorMapping, new NullScriptingEnvironment());
|
||||
}
|
||||
|
||||
// Package visible for testing
|
||||
InspectorSonarPlugin(
|
||||
ApplicationWrapper wrapper,
|
||||
DescriptorMapping descriptorMapping,
|
||||
ScriptingEnvironment scriptingEnvironment) {
|
||||
mDescriptorMapping = descriptorMapping;
|
||||
|
||||
mObjectTracker = new ObjectTracker();
|
||||
mApplication = wrapper;
|
||||
mScriptingEnvironment = scriptingEnvironment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "Inspector";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnect(SonarConnection connection) throws Exception {
|
||||
mConnection = connection;
|
||||
mDescriptorMapping.onConnect(connection);
|
||||
|
||||
ConsoleCommandReceiver.listenForCommands(
|
||||
connection,
|
||||
mScriptingEnvironment,
|
||||
new ConsoleCommandReceiver.ContextProvider() {
|
||||
@Override
|
||||
@Nullable
|
||||
public Object getObjectForId(String id) {
|
||||
return mObjectTracker.get(id);
|
||||
}
|
||||
});
|
||||
connection.receive("getRoot", mGetRoot);
|
||||
connection.receive("getNodes", mGetNodes);
|
||||
connection.receive("setData", mSetData);
|
||||
connection.receive("setHighlighted", mSetHighlighted);
|
||||
connection.receive("setSearchActive", mSetSearchActive);
|
||||
connection.receive("getSearchResults", mGetSearchResults);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisconnect() throws Exception {
|
||||
if (mHighlightedId != null) {
|
||||
setHighlighted(mHighlightedId, false);
|
||||
mHighlightedId = null;
|
||||
}
|
||||
|
||||
mObjectTracker.clear();
|
||||
mDescriptorMapping.onDisconnect();
|
||||
mConnection = null;
|
||||
}
|
||||
|
||||
final SonarReceiver mGetRoot =
|
||||
new MainThreadSonarReceiver(mConnection) {
|
||||
@Override
|
||||
public void onReceiveOnMainThread(SonarObject params, SonarResponder responder)
|
||||
throws Exception {
|
||||
responder.success(getNode(trackObject(mApplication)));
|
||||
}
|
||||
};
|
||||
|
||||
final SonarReceiver mGetNodes =
|
||||
new MainThreadSonarReceiver(mConnection) {
|
||||
@Override
|
||||
public void onReceiveOnMainThread(final SonarObject params, final SonarResponder responder)
|
||||
throws Exception {
|
||||
final SonarArray ids = params.getArray("ids");
|
||||
final SonarArray.Builder result = new SonarArray.Builder();
|
||||
|
||||
for (int i = 0, count = ids.length(); i < count; i++) {
|
||||
final String id = ids.getString(i);
|
||||
final SonarObject node = getNode(id);
|
||||
if (node != null) {
|
||||
result.put(node);
|
||||
} else {
|
||||
responder.error(
|
||||
new SonarObject.Builder()
|
||||
.put("message", "No node with given id")
|
||||
.put("id", id)
|
||||
.build());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
responder.success(new SonarObject.Builder().put("elements", result).build());
|
||||
}
|
||||
};
|
||||
|
||||
final SonarReceiver mSetData =
|
||||
new MainThreadSonarReceiver(mConnection) {
|
||||
@Override
|
||||
public void onReceiveOnMainThread(final SonarObject params, SonarResponder responder)
|
||||
throws Exception {
|
||||
final String nodeId = params.getString("id");
|
||||
final SonarArray keyPath = params.getArray("path");
|
||||
final SonarDynamic value = params.getDynamic("value");
|
||||
|
||||
final Object obj = mObjectTracker.get(nodeId);
|
||||
if (obj == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final NodeDescriptor<Object> descriptor = descriptorForObject(obj);
|
||||
if (descriptor == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final int count = keyPath.length();
|
||||
final String[] path = new String[count];
|
||||
for (int i = 0; i < count; i++) {
|
||||
path[i] = keyPath.getString(i);
|
||||
}
|
||||
|
||||
descriptor.setValue(obj, path, value);
|
||||
}
|
||||
};
|
||||
|
||||
final SonarReceiver mSetHighlighted =
|
||||
new MainThreadSonarReceiver(mConnection) {
|
||||
@Override
|
||||
public void onReceiveOnMainThread(final SonarObject params, SonarResponder responder)
|
||||
throws Exception {
|
||||
final String nodeId = params.getString("id");
|
||||
|
||||
if (mHighlightedId != null) {
|
||||
setHighlighted(mHighlightedId, false);
|
||||
}
|
||||
|
||||
if (nodeId != null) {
|
||||
setHighlighted(nodeId, true);
|
||||
}
|
||||
|
||||
mHighlightedId = nodeId;
|
||||
}
|
||||
};
|
||||
|
||||
final SonarReceiver mSetSearchActive =
|
||||
new MainThreadSonarReceiver(mConnection) {
|
||||
@Override
|
||||
public void onReceiveOnMainThread(final SonarObject params, SonarResponder responder)
|
||||
throws Exception {
|
||||
final boolean active = params.getBoolean("active");
|
||||
final List<View> roots = mApplication.getViewRoots();
|
||||
|
||||
ViewGroup root = null;
|
||||
for (int i = roots.size() - 1; i >= 0; i--) {
|
||||
if (roots.get(i) instanceof ViewGroup) {
|
||||
root = (ViewGroup) roots.get(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (root != null) {
|
||||
if (active) {
|
||||
mTouchOverlay = new TouchOverlayView(root.getContext());
|
||||
root.addView(mTouchOverlay);
|
||||
root.bringChildToFront(mTouchOverlay);
|
||||
} else {
|
||||
root.removeView(mTouchOverlay);
|
||||
mTouchOverlay = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
final SonarReceiver mGetSearchResults =
|
||||
new MainThreadSonarReceiver(mConnection) {
|
||||
@Override
|
||||
public void onReceiveOnMainThread(SonarObject params, SonarResponder responder)
|
||||
throws Exception {
|
||||
final String query = params.getString("query");
|
||||
final SearchResultNode matchTree = searchTree(query.toLowerCase(), mApplication);
|
||||
final SonarObject results = matchTree == null ? null : matchTree.toSonarObject();
|
||||
final SonarObject response =
|
||||
new SonarObject.Builder().put("results", results).put("query", query).build();
|
||||
responder.success(response);
|
||||
}
|
||||
};
|
||||
|
||||
class TouchOverlayView extends View implements HiddenNode {
|
||||
public TouchOverlayView(Context context) {
|
||||
super(context);
|
||||
setBackgroundColor(BoundsDrawable.COLOR_HIGHLIGHT_CONTENT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(final MotionEvent event) {
|
||||
if (event.getAction() != MotionEvent.ACTION_UP) {
|
||||
return true;
|
||||
}
|
||||
|
||||
new ErrorReportingRunnable(mConnection) {
|
||||
@Override
|
||||
public void runOrThrow() throws Exception {
|
||||
hitTest((int) event.getX(), (int) event.getY());
|
||||
}
|
||||
}.run();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
void hitTest(final int touchX, final int touchY) throws Exception {
|
||||
final SonarArray.Builder path = new SonarArray.Builder();
|
||||
path.put(trackObject(mApplication));
|
||||
|
||||
final Touch touch =
|
||||
new Touch() {
|
||||
int x = touchX;
|
||||
int y = touchY;
|
||||
Object node = mApplication;
|
||||
|
||||
@Override
|
||||
public void finish() {
|
||||
mConnection.send("select", new SonarObject.Builder().put("path", path).build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void continueWithOffset(
|
||||
final int childIndex, final int offsetX, final int offsetY) {
|
||||
final Touch touch = this;
|
||||
|
||||
new ErrorReportingRunnable(mConnection) {
|
||||
@Override
|
||||
protected void runOrThrow() throws Exception {
|
||||
x -= offsetX;
|
||||
y -= offsetY;
|
||||
|
||||
node = assertNotNull(descriptorForObject(node).getChildAt(node, childIndex));
|
||||
path.put(trackObject(node));
|
||||
|
||||
final NodeDescriptor<Object> descriptor = descriptorForObject(node);
|
||||
descriptor.hitTest(node, touch);
|
||||
}
|
||||
}.run();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containedIn(int l, int t, int r, int b) {
|
||||
return x >= l && x <= r && y >= t && y <= b;
|
||||
}
|
||||
};
|
||||
|
||||
final NodeDescriptor<Object> descriptor = descriptorForObject(mApplication);
|
||||
descriptor.hitTest(mApplication, touch);
|
||||
}
|
||||
|
||||
private void setHighlighted(final String id, final boolean highlighted) throws Exception {
|
||||
final Object obj = mObjectTracker.get(id);
|
||||
if (obj == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final NodeDescriptor<Object> descriptor = descriptorForObject(obj);
|
||||
if (descriptor == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
descriptor.setHighlighted(obj, highlighted);
|
||||
}
|
||||
|
||||
public SearchResultNode searchTree(String query, Object obj) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForObject(obj);
|
||||
List<SearchResultNode> childTrees = null;
|
||||
boolean isMatch = descriptor.matches(query, obj);
|
||||
|
||||
for (int i = 0; i < descriptor.getChildCount(obj); i++) {
|
||||
Object child = descriptor.getChildAt(obj, i);
|
||||
SearchResultNode childNode = searchTree(query, child);
|
||||
if (childNode != null) {
|
||||
if (childTrees == null) {
|
||||
childTrees = new ArrayList<>();
|
||||
}
|
||||
childTrees.add(childNode);
|
||||
}
|
||||
}
|
||||
|
||||
if (isMatch || childTrees != null) {
|
||||
final String id = trackObject(obj);
|
||||
return new SearchResultNode(id, isMatch, getNode(id), childTrees);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private @Nullable SonarObject getNode(String id) throws Exception {
|
||||
final Object obj = mObjectTracker.get(id);
|
||||
if (obj == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final NodeDescriptor<Object> descriptor = descriptorForObject(obj);
|
||||
if (descriptor == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final SonarArray.Builder children = new SonarArray.Builder();
|
||||
new ErrorReportingRunnable(mConnection) {
|
||||
@Override
|
||||
protected void runOrThrow() throws Exception {
|
||||
for (int i = 0, count = descriptor.getChildCount(obj); i < count; i++) {
|
||||
final Object child = assertNotNull(descriptor.getChildAt(obj, i));
|
||||
children.put(trackObject(child));
|
||||
}
|
||||
}
|
||||
}.run();
|
||||
|
||||
final SonarObject.Builder data = new SonarObject.Builder();
|
||||
new ErrorReportingRunnable(mConnection) {
|
||||
@Override
|
||||
protected void runOrThrow() throws Exception {
|
||||
for (Named<SonarObject> props : descriptor.getData(obj)) {
|
||||
data.put(props.getName(), props.getValue());
|
||||
}
|
||||
}
|
||||
}.run();
|
||||
|
||||
final SonarArray.Builder attributes = new SonarArray.Builder();
|
||||
new ErrorReportingRunnable(mConnection) {
|
||||
@Override
|
||||
protected void runOrThrow() throws Exception {
|
||||
for (Named<String> attribute : descriptor.getAttributes(obj)) {
|
||||
attributes.put(
|
||||
new SonarObject.Builder()
|
||||
.put("name", attribute.getName())
|
||||
.put("value", attribute.getValue())
|
||||
.build());
|
||||
}
|
||||
}
|
||||
}.run();
|
||||
|
||||
return new SonarObject.Builder()
|
||||
.put("id", descriptor.getId(obj))
|
||||
.put("name", descriptor.getName(obj))
|
||||
.put("data", data)
|
||||
.put("children", children)
|
||||
.put("attributes", attributes)
|
||||
.put("decoration", descriptor.getDecoration(obj))
|
||||
.build();
|
||||
}
|
||||
|
||||
private String trackObject(Object obj) throws Exception {
|
||||
final NodeDescriptor<Object> descriptor = descriptorForObject(obj);
|
||||
final String id = descriptor.getId(obj);
|
||||
final Object curr = mObjectTracker.get(id);
|
||||
if (obj != curr) {
|
||||
mObjectTracker.put(id, obj);
|
||||
descriptor.init(obj);
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
private NodeDescriptor<Object> descriptorForObject(Object obj) {
|
||||
final Class c = assertNotNull(obj).getClass();
|
||||
return (NodeDescriptor<Object>) mDescriptorMapping.descriptorForClass(c);
|
||||
}
|
||||
|
||||
private static Object assertNotNull(@Nullable Object o) {
|
||||
if (o == null) {
|
||||
throw new AssertionError("Unexpected null value");
|
||||
}
|
||||
return o;
|
||||
}
|
||||
}
|
||||
79
android/plugins/inspector/InspectorValue.java
Normal file
79
android/plugins/inspector/InspectorValue.java
Normal file
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.facebook.sonar.plugins.inspector;
|
||||
|
||||
import com.facebook.sonar.core.SonarObject;
|
||||
import com.facebook.sonar.core.SonarValue;
|
||||
|
||||
public class InspectorValue<T> implements SonarValue {
|
||||
|
||||
/**
|
||||
* Descrive the type of data this value contains. This will influence how values are parsed and
|
||||
* displayed by the Sonar desktop app. For example colors will be parse as integers and displayed
|
||||
* using hex values and be editable using a color picker.
|
||||
*
|
||||
* <p>Do not extends this list of types without adding support for the type in the desktop
|
||||
* Inspector.
|
||||
*/
|
||||
public static class Type<T> {
|
||||
|
||||
public static final Type Auto = new Type<>("auto");
|
||||
public static final Type<String> Text = new Type<>("text");
|
||||
public static final Type<Number> Number = new Type<>("number");
|
||||
public static final Type<Boolean> Boolean = new Type<>("boolean");
|
||||
public static final Type<String> Enum = new Type<>("enum");
|
||||
public static final Type<Integer> Color = new Type<>("color");
|
||||
|
||||
private final String mName;
|
||||
|
||||
Type(String name) {
|
||||
mName = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return mName;
|
||||
}
|
||||
}
|
||||
|
||||
final Type<T> mType;
|
||||
final T mValue;
|
||||
final boolean mMutable;
|
||||
|
||||
private InspectorValue(Type<T> type, T value, boolean mutable) {
|
||||
mType = type;
|
||||
mValue = value;
|
||||
mMutable = mutable;
|
||||
}
|
||||
|
||||
public static <T> InspectorValue<T> mutable(Type<T> type, T value) {
|
||||
return new InspectorValue<>(type, value, true);
|
||||
}
|
||||
|
||||
public static <T> InspectorValue<T> immutable(Type<T> type, T value) {
|
||||
return new InspectorValue<>(type, value, false);
|
||||
}
|
||||
|
||||
public static InspectorValue mutable(Object value) {
|
||||
return new InspectorValue<>(Type.Auto, value, true);
|
||||
}
|
||||
|
||||
public static InspectorValue immutable(Object value) {
|
||||
return new InspectorValue<>(Type.Auto, value, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SonarObject toSonarObject() {
|
||||
return new SonarObject.Builder()
|
||||
.put("__type__", mType)
|
||||
.put("__mutable__", mMutable)
|
||||
.put("value", mValue)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
27
android/plugins/inspector/Named.java
Normal file
27
android/plugins/inspector/Named.java
Normal file
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.facebook.sonar.plugins.inspector;
|
||||
|
||||
public class Named<ValueType> {
|
||||
private final String mName;
|
||||
private final ValueType mValue;
|
||||
|
||||
public Named(String name, ValueType value) {
|
||||
mName = name;
|
||||
mValue = value;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return mName;
|
||||
}
|
||||
|
||||
public ValueType getValue() {
|
||||
return mValue;
|
||||
}
|
||||
}
|
||||
143
android/plugins/inspector/NodeDescriptor.java
Normal file
143
android/plugins/inspector/NodeDescriptor.java
Normal file
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.facebook.sonar.plugins.inspector;
|
||||
|
||||
import com.facebook.sonar.core.SonarArray;
|
||||
import com.facebook.sonar.core.SonarConnection;
|
||||
import com.facebook.sonar.core.SonarDynamic;
|
||||
import com.facebook.sonar.core.SonarObject;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A NodeDescriptor is an object which known how to expose an Object of type T to the ew Inspector.
|
||||
* This class is the extension point for the Sonar inspector plugin and is how custom classes and
|
||||
* data can be exposed to the inspector.
|
||||
*/
|
||||
public abstract class NodeDescriptor<T> {
|
||||
private SonarConnection mConnection;
|
||||
private DescriptorMapping mDescriptorMapping;
|
||||
|
||||
void setConnection(SonarConnection connection) {
|
||||
mConnection = connection;
|
||||
}
|
||||
|
||||
void setDescriptorMapping(DescriptorMapping descriptorMapping) {
|
||||
mDescriptorMapping = descriptorMapping;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The descriptor for a given class. This is useful for when a descriptor wants to
|
||||
* delegate parts of its implementation to another descriptor, say the super class of the
|
||||
* object it describes. This is highly encouraged instead of subclassing another descriptor
|
||||
* class.
|
||||
*/
|
||||
protected final NodeDescriptor<?> descriptorForClass(Class<?> clazz) {
|
||||
return mDescriptorMapping.descriptorForClass(clazz);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate a node. This tells Sonar that this node is no longer valid and its properties and/or
|
||||
* children have changed. This will trigger Sonar to re-query this node getting any new data.
|
||||
*/
|
||||
protected final void invalidate(final T node) {
|
||||
if (mConnection != null) {
|
||||
new ErrorReportingRunnable() {
|
||||
@Override
|
||||
protected void runOrThrow() throws Exception {
|
||||
SonarArray array =
|
||||
new SonarArray.Builder()
|
||||
.put(new SonarObject.Builder().put("id", getId(node)).build())
|
||||
.build();
|
||||
SonarObject params = new SonarObject.Builder().put("nodes", array).build();
|
||||
mConnection.send("invalidate", params);
|
||||
}
|
||||
}.run();
|
||||
}
|
||||
}
|
||||
|
||||
protected final boolean connected() {
|
||||
return mConnection != null;
|
||||
}
|
||||
|
||||
protected abstract class ErrorReportingRunnable
|
||||
extends com.facebook.sonar.core.ErrorReportingRunnable {
|
||||
public ErrorReportingRunnable() {
|
||||
super(mConnection);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a node. This implementation usually consists of setting up listeners to know when to
|
||||
* call {@link NodeDescriptor#invalidate(Object)}.
|
||||
*/
|
||||
public abstract void init(T node) throws Exception;
|
||||
|
||||
/**
|
||||
* A globally unique ID used to identify a node in a hierarchy. If your node does not have a
|
||||
* globally unique ID it is fine to rely on {@link System#identityHashCode(Object)}.
|
||||
*/
|
||||
public abstract String getId(T node) throws Exception;
|
||||
|
||||
/**
|
||||
* The name used to identify this node in the inspector. Does not need to be unique. A good
|
||||
* default is to use the class name of the node.
|
||||
*/
|
||||
public abstract String getName(T node) throws Exception;
|
||||
|
||||
/** @return The number of children this node exposes in the inspector. */
|
||||
public abstract int getChildCount(T node) throws Exception;
|
||||
|
||||
/** @return The child at index. */
|
||||
public abstract Object getChildAt(T node, int index) throws Exception;
|
||||
|
||||
/**
|
||||
* Get the data to show for this node in the sidebar of the inspector. The object will be showen
|
||||
* in order and with a header matching the given name.
|
||||
*/
|
||||
public abstract List<Named<SonarObject>> getData(T node) throws Exception;
|
||||
|
||||
/**
|
||||
* Set a value on the provided node at the given path. The path will match a key path in the data
|
||||
* provided by {@link this#getData(Object)} and the value will be of the same type as the value
|
||||
* mathcing that path in the returned object.
|
||||
*/
|
||||
public abstract void setValue(T node, String[] path, SonarDynamic value) throws Exception;
|
||||
|
||||
/**
|
||||
* Get the attributes for this node. This is a list of read-only string:string mapping which show
|
||||
* up inline in the elements inspector. See {@link Named} for more information.
|
||||
*/
|
||||
public abstract List<Named<String>> getAttributes(T node) throws Exception;
|
||||
|
||||
/**
|
||||
* Highlight this node. Use {@link HighlightedOverlay} if possible. This is used to highlight a
|
||||
* node which is selected in the inspector. The plugin automatically takes care of de-selecting
|
||||
* the previously highlighted node.
|
||||
*/
|
||||
public abstract void setHighlighted(T node, boolean selected) throws Exception;
|
||||
|
||||
/**
|
||||
* Perform hit testing on the given node. Either continue the search in a child with {@link
|
||||
* Touch#continueWithOffset(int, int, int)} or finish the hit testing on this node with {@link
|
||||
* Touch#finish()}
|
||||
*/
|
||||
public abstract void hitTest(T node, Touch touch) throws Exception;
|
||||
|
||||
/**
|
||||
* @return A string indicating how this element should be decorated. Check with the Sonar desktop
|
||||
* app to see what values are supported.
|
||||
*/
|
||||
public abstract String getDecoration(T node) throws Exception;
|
||||
|
||||
/**
|
||||
* Test this node against a given query to see if it matches. This is used for finding search
|
||||
* results.
|
||||
*/
|
||||
public abstract boolean matches(String query, T node) throws Exception;
|
||||
}
|
||||
45
android/plugins/inspector/ObjectTracker.java
Normal file
45
android/plugins/inspector/ObjectTracker.java
Normal file
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.facebook.sonar.plugins.inspector;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
class ObjectTracker {
|
||||
private final Map<String, WeakReference<Object>> mObjects = new HashMap<>();
|
||||
|
||||
void put(String id, Object obj) {
|
||||
mObjects.put(id, new WeakReference<>(obj));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
Object get(String id) {
|
||||
final WeakReference<Object> weakObj = mObjects.get(id);
|
||||
if (weakObj == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final Object obj = weakObj.get();
|
||||
if (obj == null) {
|
||||
mObjects.remove(id);
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
void clear() {
|
||||
mObjects.clear();
|
||||
}
|
||||
|
||||
boolean contains(String id) {
|
||||
return mObjects.containsKey(id);
|
||||
}
|
||||
}
|
||||
51
android/plugins/inspector/SearchResultNode.java
Normal file
51
android/plugins/inspector/SearchResultNode.java
Normal file
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.facebook.sonar.plugins.inspector;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
import com.facebook.sonar.core.SonarArray;
|
||||
import com.facebook.sonar.core.SonarObject;
|
||||
import java.util.List;
|
||||
|
||||
public class SearchResultNode {
|
||||
|
||||
private final String id;
|
||||
private final boolean isMatch;
|
||||
private final SonarObject element;
|
||||
@Nullable
|
||||
private final List<SearchResultNode> children;
|
||||
|
||||
SearchResultNode(
|
||||
String id, boolean isMatch, SonarObject element, @Nullable List<SearchResultNode> children) {
|
||||
this.id = id;
|
||||
this.isMatch = isMatch;
|
||||
this.element = element;
|
||||
this.children = children;
|
||||
}
|
||||
|
||||
SonarObject toSonarObject() {
|
||||
final SonarArray childArray;
|
||||
if (children != null) {
|
||||
final SonarArray.Builder builder = new SonarArray.Builder();
|
||||
for (SearchResultNode child : children) {
|
||||
builder.put(child.toSonarObject());
|
||||
}
|
||||
childArray = builder.build();
|
||||
} else {
|
||||
childArray = null;
|
||||
}
|
||||
|
||||
return new SonarObject.Builder()
|
||||
.put("id", this.id)
|
||||
.put("isMatch", this.isMatch)
|
||||
.put("element", this.element)
|
||||
.put("children", childArray)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
14
android/plugins/inspector/SelfRegisteringNodeDescriptor.java
Normal file
14
android/plugins/inspector/SelfRegisteringNodeDescriptor.java
Normal file
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.facebook.sonar.plugins.inspector;
|
||||
|
||||
public abstract class SelfRegisteringNodeDescriptor<T> extends NodeDescriptor<T> {
|
||||
|
||||
public abstract void register(DescriptorMapping descriptorMapping);
|
||||
}
|
||||
31
android/plugins/inspector/Touch.java
Normal file
31
android/plugins/inspector/Touch.java
Normal file
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.facebook.sonar.plugins.inspector;
|
||||
|
||||
/**
|
||||
* Used to collect the path from the root node to the node at point [x, y]. This is used for
|
||||
* creating a node path to the targeting node from the root when performing hit testing.
|
||||
*/
|
||||
public interface Touch {
|
||||
|
||||
/**
|
||||
* Call this when the path has reached its destination. This should be called from the descriptor
|
||||
* which described the leaf node containing [x, y].
|
||||
*/
|
||||
void finish();
|
||||
|
||||
/**
|
||||
* Continue hit testing in child at the given index. Offseting the touch location to the child's
|
||||
* coordinate system.
|
||||
*/
|
||||
void continueWithOffset(int childIndex, int offsetX, int offsetY);
|
||||
|
||||
/** @return Whether or not this Touch is contained within the provided bounds. */
|
||||
boolean containedIn(int l, int t, int r, int b);
|
||||
}
|
||||
129
android/plugins/inspector/descriptors/ActivityDescriptor.java
Normal file
129
android/plugins/inspector/descriptors/ActivityDescriptor.java
Normal file
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.facebook.sonar.plugins.inspector.descriptors;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.view.Window;
|
||||
import com.facebook.sonar.core.SonarDynamic;
|
||||
import com.facebook.sonar.core.SonarObject;
|
||||
import com.facebook.sonar.plugins.inspector.Named;
|
||||
import com.facebook.sonar.plugins.inspector.NodeDescriptor;
|
||||
import com.facebook.sonar.plugins.inspector.Touch;
|
||||
import com.facebook.stetho.common.android.FragmentActivityAccessor;
|
||||
import com.facebook.stetho.common.android.FragmentCompat;
|
||||
import com.facebook.stetho.common.android.FragmentManagerAccessor;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class ActivityDescriptor extends NodeDescriptor<Activity> {
|
||||
|
||||
@Override
|
||||
public void init(Activity node) {}
|
||||
|
||||
@Override
|
||||
public String getId(Activity node) {
|
||||
return Integer.toString(System.identityHashCode(node));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName(Activity node) {
|
||||
return node.getClass().getSimpleName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getChildCount(Activity node) {
|
||||
return (node.getWindow() != null ? 1 : 0)
|
||||
+ getDialogFragments(FragmentCompat.getSupportLibInstance(), node).size()
|
||||
+ getDialogFragments(FragmentCompat.getFrameworkInstance(), node).size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getChildAt(Activity node, int index) {
|
||||
if (node.getWindow() != null) {
|
||||
if (index == 0) {
|
||||
return node.getWindow();
|
||||
} else {
|
||||
index--;
|
||||
}
|
||||
}
|
||||
|
||||
final List dialogs = getDialogFragments(FragmentCompat.getSupportLibInstance(), node);
|
||||
if (index < dialogs.size()) {
|
||||
return dialogs.get(index);
|
||||
} else {
|
||||
final List supportDialogs = getDialogFragments(FragmentCompat.getFrameworkInstance(), node);
|
||||
return supportDialogs.get(index - dialogs.size());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Named<SonarObject>> getData(Activity node) {
|
||||
return Collections.EMPTY_LIST;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(Activity node, String[] path, SonarDynamic value) throws Exception {}
|
||||
|
||||
@Override
|
||||
public List<Named<String>> getAttributes(Activity node) {
|
||||
return Collections.EMPTY_LIST;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setHighlighted(Activity node, boolean selected) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(Window.class);
|
||||
descriptor.setHighlighted(node.getWindow(), selected);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hitTest(Activity node, Touch touch) {
|
||||
touch.continueWithOffset(0, 0, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable String getDecoration(Activity obj) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(String query, Activity node) throws Exception {
|
||||
NodeDescriptor descriptor = descriptorForClass(Object.class);
|
||||
return descriptor.matches(query, node);
|
||||
}
|
||||
|
||||
private static List<Object> getDialogFragments(FragmentCompat compat, Activity activity) {
|
||||
if (compat == null || !compat.getFragmentActivityClass().isInstance(activity)) {
|
||||
return Collections.EMPTY_LIST;
|
||||
}
|
||||
|
||||
FragmentActivityAccessor activityAccessor = compat.forFragmentActivity();
|
||||
Object fragmentManager = activityAccessor.getFragmentManager(activity);
|
||||
if (fragmentManager == null) {
|
||||
return Collections.EMPTY_LIST;
|
||||
}
|
||||
|
||||
FragmentManagerAccessor fragmentManagerAccessor = compat.forFragmentManager();
|
||||
List<Object> addedFragments = fragmentManagerAccessor.getAddedFragments(fragmentManager);
|
||||
if (addedFragments == null) {
|
||||
return Collections.EMPTY_LIST;
|
||||
}
|
||||
|
||||
final List<Object> dialogFragments = new ArrayList<>();
|
||||
for (int i = 0, N = addedFragments.size(); i < N; ++i) {
|
||||
final Object fragment = addedFragments.get(i);
|
||||
if (compat.getDialogFragmentClass().isInstance(fragment)) {
|
||||
dialogFragments.add(fragment);
|
||||
}
|
||||
}
|
||||
|
||||
return dialogFragments;
|
||||
}
|
||||
}
|
||||
161
android/plugins/inspector/descriptors/ApplicationDescriptor.java
Normal file
161
android/plugins/inspector/descriptors/ApplicationDescriptor.java
Normal file
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.facebook.sonar.plugins.inspector.descriptors;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import com.facebook.sonar.core.SonarDynamic;
|
||||
import com.facebook.sonar.core.SonarObject;
|
||||
import com.facebook.sonar.plugins.inspector.ApplicationWrapper;
|
||||
import com.facebook.sonar.plugins.inspector.Named;
|
||||
import com.facebook.sonar.plugins.inspector.NodeDescriptor;
|
||||
import com.facebook.sonar.plugins.inspector.Touch;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class ApplicationDescriptor extends NodeDescriptor<ApplicationWrapper> {
|
||||
|
||||
private class NodeKey {
|
||||
private int[] mKey;
|
||||
|
||||
boolean set(ApplicationWrapper node) {
|
||||
final List<View> roots = node.getViewRoots();
|
||||
final int childCount = roots.size();
|
||||
final int[] key = new int[childCount];
|
||||
|
||||
for (int i = 0; i < childCount; i++) {
|
||||
final View child = roots.get(i);
|
||||
key[i] = System.identityHashCode(child);
|
||||
}
|
||||
|
||||
boolean changed = false;
|
||||
if (mKey == null) {
|
||||
changed = true;
|
||||
} else if (mKey.length != key.length) {
|
||||
changed = true;
|
||||
} else {
|
||||
for (int i = 0; i < childCount; i++) {
|
||||
if (mKey[i] != key[i]) {
|
||||
changed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mKey = key;
|
||||
return changed;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(final ApplicationWrapper node) {
|
||||
node.setListener(
|
||||
new ApplicationWrapper.ActivityStackChangedListener() {
|
||||
@Override
|
||||
public void onActivityStackChanged(List<Activity> stack) {
|
||||
invalidate(node);
|
||||
}
|
||||
});
|
||||
|
||||
final NodeKey key = new NodeKey();
|
||||
final Runnable maybeInvalidate =
|
||||
new NodeDescriptor.ErrorReportingRunnable() {
|
||||
@Override
|
||||
public void runOrThrow() throws Exception {
|
||||
if (connected()) {
|
||||
if (key.set(node)) {
|
||||
invalidate(node);
|
||||
}
|
||||
node.postDelayed(this, 1000);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
node.postDelayed(maybeInvalidate, 1000);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId(ApplicationWrapper node) {
|
||||
return node.getApplication().getPackageName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName(ApplicationWrapper node) {
|
||||
return node.getApplication().getPackageName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getChildCount(ApplicationWrapper node) {
|
||||
return node.getViewRoots().size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getChildAt(ApplicationWrapper node, int index) {
|
||||
final View view = node.getViewRoots().get(index);
|
||||
|
||||
for (Activity activity : node.getActivityStack()) {
|
||||
if (activity.getWindow().getDecorView() == view) {
|
||||
return activity;
|
||||
}
|
||||
}
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Named<SonarObject>> getData(ApplicationWrapper node) {
|
||||
return Collections.EMPTY_LIST;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(ApplicationWrapper node, String[] path, SonarDynamic value) {}
|
||||
|
||||
@Override
|
||||
public List<Named<String>> getAttributes(ApplicationWrapper node) {
|
||||
return Collections.EMPTY_LIST;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setHighlighted(ApplicationWrapper node, boolean selected) throws Exception {
|
||||
final int childCount = getChildCount(node);
|
||||
if (childCount > 0) {
|
||||
final Object topChild = getChildAt(node, childCount - 1);
|
||||
final NodeDescriptor descriptor = descriptorForClass(topChild.getClass());
|
||||
descriptor.setHighlighted(topChild, selected);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hitTest(ApplicationWrapper node, Touch touch) {
|
||||
final int childCount = getChildCount(node);
|
||||
|
||||
for (int i = childCount - 1; i >= 0; i--) {
|
||||
final Object child = getChildAt(node, i);
|
||||
if (child instanceof Activity || child instanceof ViewGroup) {
|
||||
touch.continueWithOffset(i, 0, 0);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
touch.finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable String getDecoration(ApplicationWrapper obj) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(String query, ApplicationWrapper node) throws Exception {
|
||||
NodeDescriptor descriptor = descriptorForClass(Object.class);
|
||||
return descriptor.matches(query, node);
|
||||
}
|
||||
}
|
||||
81
android/plugins/inspector/descriptors/DialogDescriptor.java
Normal file
81
android/plugins/inspector/descriptors/DialogDescriptor.java
Normal file
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.facebook.sonar.plugins.inspector.descriptors;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.view.Window;
|
||||
import com.facebook.sonar.core.SonarDynamic;
|
||||
import com.facebook.sonar.core.SonarObject;
|
||||
import com.facebook.sonar.plugins.inspector.Named;
|
||||
import com.facebook.sonar.plugins.inspector.NodeDescriptor;
|
||||
import com.facebook.sonar.plugins.inspector.Touch;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class DialogDescriptor extends NodeDescriptor<Dialog> {
|
||||
|
||||
@Override
|
||||
public void init(Dialog node) {}
|
||||
|
||||
@Override
|
||||
public String getId(Dialog node) {
|
||||
return Integer.toString(System.identityHashCode(node));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName(Dialog node) {
|
||||
return node.getClass().getSimpleName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getChildCount(Dialog node) {
|
||||
return node.getWindow() == null ? 0 : 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getChildAt(Dialog node, int index) {
|
||||
return node.getWindow();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Named<SonarObject>> getData(Dialog node) {
|
||||
return Collections.EMPTY_LIST;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(Dialog node, String[] path, SonarDynamic value) {}
|
||||
|
||||
@Override
|
||||
public List<Named<String>> getAttributes(Dialog node) {
|
||||
return Collections.EMPTY_LIST;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setHighlighted(Dialog node, boolean selected) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(Window.class);
|
||||
descriptor.setHighlighted(node.getWindow(), selected);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hitTest(Dialog node, Touch touch) {
|
||||
touch.continueWithOffset(0, 0, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable String getDecoration(Dialog obj) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(String query, Dialog node) throws Exception {
|
||||
NodeDescriptor descriptor = descriptorForClass(Object.class);
|
||||
return descriptor.matches(query, node);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.facebook.sonar.plugins.inspector.descriptors;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.app.DialogFragment;
|
||||
import android.app.Fragment;
|
||||
import com.facebook.sonar.core.SonarDynamic;
|
||||
import com.facebook.sonar.core.SonarObject;
|
||||
import com.facebook.sonar.plugins.inspector.Named;
|
||||
import com.facebook.sonar.plugins.inspector.NodeDescriptor;
|
||||
import com.facebook.sonar.plugins.inspector.Touch;
|
||||
import java.util.List;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class DialogFragmentDescriptor extends NodeDescriptor<DialogFragment> {
|
||||
|
||||
@Override
|
||||
public void init(DialogFragment node) {}
|
||||
|
||||
@Override
|
||||
public String getId(DialogFragment node) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(Fragment.class);
|
||||
return descriptor.getId(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName(DialogFragment node) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(Fragment.class);
|
||||
return descriptor.getName(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getChildCount(DialogFragment node) {
|
||||
return node.getDialog() == null ? 0 : 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getChildAt(DialogFragment node, int index) {
|
||||
return node.getDialog();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Named<SonarObject>> getData(DialogFragment node) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(Fragment.class);
|
||||
return descriptor.getData(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(DialogFragment node, String[] path, SonarDynamic value) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(Fragment.class);
|
||||
descriptor.setValue(node, path, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Named<String>> getAttributes(DialogFragment node) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(Fragment.class);
|
||||
return descriptor.getAttributes(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setHighlighted(DialogFragment node, boolean selected) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(Dialog.class);
|
||||
if (node.getDialog() != null) {
|
||||
descriptor.setHighlighted(node.getDialog(), selected);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hitTest(DialogFragment node, Touch touch) {
|
||||
touch.continueWithOffset(0, 0, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable String getDecoration(DialogFragment obj) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(String query, DialogFragment node) throws Exception {
|
||||
NodeDescriptor descriptor = descriptorForClass(Object.class);
|
||||
return descriptor.matches(query, node);
|
||||
}
|
||||
}
|
||||
146
android/plugins/inspector/descriptors/DrawableDescriptor.java
Normal file
146
android/plugins/inspector/descriptors/DrawableDescriptor.java
Normal file
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.facebook.sonar.plugins.inspector.descriptors;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.view.View;
|
||||
import com.facebook.sonar.core.SonarDynamic;
|
||||
import com.facebook.sonar.core.SonarObject;
|
||||
import com.facebook.sonar.plugins.inspector.HighlightedOverlay;
|
||||
import com.facebook.sonar.plugins.inspector.InspectorValue;
|
||||
import com.facebook.sonar.plugins.inspector.Named;
|
||||
import com.facebook.sonar.plugins.inspector.NodeDescriptor;
|
||||
import com.facebook.sonar.plugins.inspector.Touch;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class DrawableDescriptor extends NodeDescriptor<Drawable> {
|
||||
|
||||
@Override
|
||||
public void init(Drawable node) {}
|
||||
|
||||
@Override
|
||||
public String getId(Drawable node) {
|
||||
return Integer.toString(System.identityHashCode(node));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName(Drawable node) {
|
||||
return node.getClass().getSimpleName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getChildCount(Drawable node) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Object getChildAt(Drawable node, int index) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Named<SonarObject>> getData(Drawable node) {
|
||||
final SonarObject.Builder props = new SonarObject.Builder();
|
||||
final Rect bounds = node.getBounds();
|
||||
|
||||
props.put("left", InspectorValue.mutable(bounds.left));
|
||||
props.put("top", InspectorValue.mutable(bounds.top));
|
||||
props.put("right", InspectorValue.mutable(bounds.right));
|
||||
props.put("bottom", InspectorValue.mutable(bounds.bottom));
|
||||
|
||||
if (hasAlphaSupport()) {
|
||||
props.put("alpha", InspectorValue.mutable(node.getAlpha()));
|
||||
}
|
||||
|
||||
return Arrays.asList(new Named<>("Drawable", props.build()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(Drawable node, String[] path, SonarDynamic value) {
|
||||
final Rect bounds = node.getBounds();
|
||||
|
||||
switch (path[0]) {
|
||||
case "Drawable":
|
||||
switch (path[1]) {
|
||||
case "left":
|
||||
bounds.left = value.asInt();
|
||||
node.setBounds(bounds);
|
||||
break;
|
||||
case "top":
|
||||
bounds.top = value.asInt();
|
||||
node.setBounds(bounds);
|
||||
break;
|
||||
case "right":
|
||||
bounds.right = value.asInt();
|
||||
node.setBounds(bounds);
|
||||
break;
|
||||
case "bottom":
|
||||
bounds.bottom = value.asInt();
|
||||
node.setBounds(bounds);
|
||||
break;
|
||||
case "alpha":
|
||||
node.setAlpha(value.asInt());
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean hasAlphaSupport() {
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Named<String>> getAttributes(Drawable node) {
|
||||
return Collections.EMPTY_LIST;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setHighlighted(Drawable node, boolean selected) {
|
||||
// Ensure we handle wrapping drawable
|
||||
Drawable.Callback callbacks = node.getCallback();
|
||||
while (callbacks instanceof Drawable) {
|
||||
callbacks = ((Drawable) callbacks).getCallback();
|
||||
}
|
||||
|
||||
if (!(callbacks instanceof View)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final View callbackView = (View) callbacks;
|
||||
if (selected) {
|
||||
final Rect zero = new Rect();
|
||||
final Rect bounds = node.getBounds();
|
||||
HighlightedOverlay.setHighlighted(callbackView, zero, zero, bounds);
|
||||
} else {
|
||||
HighlightedOverlay.removeHighlight(callbackView);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hitTest(Drawable node, Touch touch) {
|
||||
touch.finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable String getDecoration(Drawable obj) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(String query, Drawable node) throws Exception {
|
||||
NodeDescriptor descriptor = descriptorForClass(Object.class);
|
||||
return descriptor.matches(query, node);
|
||||
}
|
||||
}
|
||||
124
android/plugins/inspector/descriptors/FragmentDescriptor.java
Normal file
124
android/plugins/inspector/descriptors/FragmentDescriptor.java
Normal file
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.facebook.sonar.plugins.inspector.descriptors;
|
||||
|
||||
import android.app.Fragment;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import com.facebook.sonar.core.SonarDynamic;
|
||||
import com.facebook.sonar.core.SonarObject;
|
||||
import com.facebook.sonar.plugins.inspector.Named;
|
||||
import com.facebook.sonar.plugins.inspector.NodeDescriptor;
|
||||
import com.facebook.sonar.plugins.inspector.Touch;
|
||||
import com.facebook.stetho.common.android.ResourcesUtil;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class FragmentDescriptor extends NodeDescriptor<Fragment> {
|
||||
|
||||
@Override
|
||||
public void init(Fragment node) {}
|
||||
|
||||
@Override
|
||||
public String getId(Fragment node) {
|
||||
return Integer.toString(System.identityHashCode(node));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName(Fragment node) {
|
||||
return node.getClass().getSimpleName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getChildCount(Fragment node) {
|
||||
return node.getView() == null ? 0 : 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getChildAt(Fragment node, int index) {
|
||||
return node.getView();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Named<SonarObject>> getData(Fragment node) {
|
||||
final Bundle args = node.getArguments();
|
||||
if (args == null || args.isEmpty()) {
|
||||
return Collections.EMPTY_LIST;
|
||||
}
|
||||
|
||||
final SonarObject.Builder bundle = new SonarObject.Builder();
|
||||
|
||||
for (String key : args.keySet()) {
|
||||
bundle.put(key, args.get(key));
|
||||
}
|
||||
|
||||
return Arrays.asList(new Named<>("Arguments", bundle.build()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(Fragment node, String[] path, SonarDynamic value) {}
|
||||
|
||||
@Override
|
||||
public List<Named<String>> getAttributes(Fragment node) {
|
||||
final String resourceId = getResourceId(node);
|
||||
|
||||
if (resourceId == null) {
|
||||
return Collections.EMPTY_LIST;
|
||||
}
|
||||
|
||||
return Arrays.asList(new Named<>("id", resourceId));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static String getResourceId(Fragment node) {
|
||||
final int id = node.getId();
|
||||
|
||||
if (id == View.NO_ID || node.getHost() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ResourcesUtil.getIdStringQuietly(node.getContext(), node.getResources(), id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setHighlighted(Fragment node, boolean selected) throws Exception {
|
||||
if (node.getView() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final NodeDescriptor descriptor = descriptorForClass(View.class);
|
||||
descriptor.setHighlighted(node.getView(), selected);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hitTest(Fragment node, Touch touch) {
|
||||
touch.continueWithOffset(0, 0, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable String getDecoration(Fragment obj) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(String query, Fragment node) throws Exception {
|
||||
final String resourceId = getResourceId(node);
|
||||
|
||||
if (resourceId != null) {
|
||||
if (resourceId.toLowerCase().contains(query)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
final NodeDescriptor objectDescriptor = descriptorForClass(Object.class);
|
||||
return objectDescriptor.matches(query, node);
|
||||
}
|
||||
}
|
||||
85
android/plugins/inspector/descriptors/ObjectDescriptor.java
Normal file
85
android/plugins/inspector/descriptors/ObjectDescriptor.java
Normal file
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.facebook.sonar.plugins.inspector.descriptors;
|
||||
|
||||
import com.facebook.sonar.core.SonarDynamic;
|
||||
import com.facebook.sonar.core.SonarObject;
|
||||
import com.facebook.sonar.plugins.inspector.Named;
|
||||
import com.facebook.sonar.plugins.inspector.NodeDescriptor;
|
||||
import com.facebook.sonar.plugins.inspector.Touch;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class ObjectDescriptor extends NodeDescriptor<Object> {
|
||||
|
||||
@Override
|
||||
public void init(Object node) {}
|
||||
|
||||
@Override
|
||||
public String getId(Object node) {
|
||||
return Integer.toString(System.identityHashCode(node));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName(Object node) {
|
||||
return node.getClass().getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getChildCount(Object node) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Object getChildAt(Object node, int index) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Named<SonarObject>> getData(Object node) {
|
||||
return Collections.EMPTY_LIST;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(Object node, String[] path, SonarDynamic value) {}
|
||||
|
||||
@Override
|
||||
public List<Named<String>> getAttributes(Object node) {
|
||||
return Collections.EMPTY_LIST;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setHighlighted(Object node, boolean selected) {}
|
||||
|
||||
@Override
|
||||
public void hitTest(Object node, Touch touch) {
|
||||
touch.finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable String getDecoration(Object obj) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(String query, Object node) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(node.getClass());
|
||||
final List<Named<String>> attributes = descriptor.getAttributes(node);
|
||||
for (Named<String> namedString : attributes) {
|
||||
if (namedString.getName().equals("id")) {
|
||||
if (namedString.getValue().toLowerCase().contains(query)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return descriptor.getName(node).toLowerCase().contains(query);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.facebook.sonar.plugins.inspector.descriptors;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.support.v4.app.DialogFragment;
|
||||
import android.support.v4.app.Fragment;
|
||||
import com.facebook.sonar.core.SonarDynamic;
|
||||
import com.facebook.sonar.core.SonarObject;
|
||||
import com.facebook.sonar.plugins.inspector.Named;
|
||||
import com.facebook.sonar.plugins.inspector.NodeDescriptor;
|
||||
import com.facebook.sonar.plugins.inspector.Touch;
|
||||
import java.util.List;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class SupportDialogFragmentDescriptor extends NodeDescriptor<DialogFragment> {
|
||||
|
||||
@Override
|
||||
public void init(DialogFragment node) {}
|
||||
|
||||
@Override
|
||||
public String getId(DialogFragment node) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(Fragment.class);
|
||||
return descriptor.getId(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName(DialogFragment node) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(Fragment.class);
|
||||
return descriptor.getName(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getChildCount(DialogFragment node) {
|
||||
return node.getDialog() == null ? 0 : 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getChildAt(DialogFragment node, int index) {
|
||||
return node.getDialog();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Named<SonarObject>> getData(DialogFragment node) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(Fragment.class);
|
||||
return descriptor.getData(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(DialogFragment node, String[] path, SonarDynamic value) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(Fragment.class);
|
||||
descriptor.setValue(node, path, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Named<String>> getAttributes(DialogFragment node) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(Fragment.class);
|
||||
return descriptor.getAttributes(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setHighlighted(DialogFragment node, boolean selected) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(Dialog.class);
|
||||
if (node.getDialog() != null) {
|
||||
descriptor.setHighlighted(node.getDialog(), selected);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hitTest(DialogFragment node, Touch touch) {
|
||||
touch.continueWithOffset(0, 0, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable String getDecoration(DialogFragment obj) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(String query, DialogFragment node) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(Object.class);
|
||||
return descriptor.matches(query, node);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.facebook.sonar.plugins.inspector.descriptors;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.view.View;
|
||||
import com.facebook.sonar.core.SonarDynamic;
|
||||
import com.facebook.sonar.core.SonarObject;
|
||||
import com.facebook.sonar.plugins.inspector.Named;
|
||||
import com.facebook.sonar.plugins.inspector.NodeDescriptor;
|
||||
import com.facebook.sonar.plugins.inspector.Touch;
|
||||
import com.facebook.stetho.common.android.ResourcesUtil;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class SupportFragmentDescriptor extends NodeDescriptor<Fragment> {
|
||||
|
||||
@Override
|
||||
public void init(Fragment node) {}
|
||||
|
||||
@Override
|
||||
public String getId(Fragment node) {
|
||||
return Integer.toString(System.identityHashCode(node));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName(Fragment node) {
|
||||
return node.getClass().getSimpleName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getChildCount(Fragment node) {
|
||||
return node.getView() == null ? 0 : 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Object getChildAt(Fragment node, int index) {
|
||||
return node.getView();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Named<SonarObject>> getData(Fragment node) {
|
||||
final Bundle args = node.getArguments();
|
||||
if (args == null || args.isEmpty()) {
|
||||
return Collections.EMPTY_LIST;
|
||||
}
|
||||
|
||||
final SonarObject.Builder bundle = new SonarObject.Builder();
|
||||
|
||||
for (String key : args.keySet()) {
|
||||
bundle.put(key, args.get(key));
|
||||
}
|
||||
|
||||
return Arrays.asList(new Named<>("Arguments", bundle.build()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(Fragment node, String[] path, SonarDynamic value) {}
|
||||
|
||||
@Override
|
||||
public List<Named<String>> getAttributes(Fragment node) {
|
||||
final int id = node.getId();
|
||||
if (id == View.NO_ID || node.getHost() == null) {
|
||||
return Collections.EMPTY_LIST;
|
||||
}
|
||||
|
||||
return Arrays.asList(
|
||||
new Named<>(
|
||||
"id", ResourcesUtil.getIdStringQuietly(node.getContext(), node.getResources(), id)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setHighlighted(Fragment node, boolean selected) throws Exception {
|
||||
if (node.getView() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final NodeDescriptor descriptor = descriptorForClass(View.class);
|
||||
descriptor.setHighlighted(node.getView(), selected);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hitTest(Fragment node, Touch touch) {
|
||||
touch.continueWithOffset(0, 0, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable String getDecoration(Fragment obj) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(String query, Fragment node) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(Object.class);
|
||||
return descriptor.matches(query, node);
|
||||
}
|
||||
}
|
||||
132
android/plugins/inspector/descriptors/TextViewDescriptor.java
Normal file
132
android/plugins/inspector/descriptors/TextViewDescriptor.java
Normal file
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.facebook.sonar.plugins.inspector.descriptors;
|
||||
|
||||
import static com.facebook.sonar.plugins.inspector.InspectorValue.Type.Color;
|
||||
import static com.facebook.sonar.plugins.inspector.InspectorValue.Type.Number;
|
||||
import static com.facebook.sonar.plugins.inspector.InspectorValue.Type.Text;
|
||||
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
import com.facebook.sonar.core.SonarDynamic;
|
||||
import com.facebook.sonar.core.SonarObject;
|
||||
import com.facebook.sonar.plugins.inspector.InspectorValue;
|
||||
import com.facebook.sonar.plugins.inspector.Named;
|
||||
import com.facebook.sonar.plugins.inspector.NodeDescriptor;
|
||||
import com.facebook.sonar.plugins.inspector.Touch;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class TextViewDescriptor extends NodeDescriptor<TextView> {
|
||||
|
||||
@Override
|
||||
public void init(TextView node) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(View.class);
|
||||
descriptor.init(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId(TextView node) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(View.class);
|
||||
return descriptor.getId(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName(TextView node) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(View.class);
|
||||
return descriptor.getName(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getChildCount(TextView node) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Object getChildAt(TextView node, int index) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Named<SonarObject>> getData(TextView node) throws Exception {
|
||||
final List<Named<SonarObject>> props = new ArrayList<>();
|
||||
final NodeDescriptor descriptor = descriptorForClass(View.class);
|
||||
|
||||
props.add(
|
||||
0,
|
||||
new Named<>(
|
||||
"TextView",
|
||||
new SonarObject.Builder()
|
||||
.put("text", InspectorValue.mutable(Text, node.getText().toString()))
|
||||
.put(
|
||||
"textColor",
|
||||
InspectorValue.mutable(Color, node.getTextColors().getDefaultColor()))
|
||||
.put("textSize", InspectorValue.mutable(Number, node.getTextSize()))
|
||||
.build()));
|
||||
|
||||
props.addAll(descriptor.getData(node));
|
||||
|
||||
return props;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(TextView node, String[] path, SonarDynamic value) throws Exception {
|
||||
switch (path[0]) {
|
||||
case "TextView":
|
||||
switch (path[1]) {
|
||||
case "text":
|
||||
node.setText(value.asString());
|
||||
break;
|
||||
case "textColor":
|
||||
node.setTextColor(value.asInt());
|
||||
break;
|
||||
case "textSize":
|
||||
node.setTextSize(value.asInt());
|
||||
break;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
final NodeDescriptor descriptor = descriptorForClass(View.class);
|
||||
descriptor.setValue(node, path, value);
|
||||
break;
|
||||
}
|
||||
invalidate(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Named<String>> getAttributes(TextView node) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(View.class);
|
||||
return descriptor.getAttributes(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setHighlighted(TextView node, boolean selected) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(View.class);
|
||||
descriptor.setHighlighted(node, selected);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hitTest(TextView node, Touch touch) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(View.class);
|
||||
descriptor.hitTest(node, touch);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable String getDecoration(TextView node) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(View.class);
|
||||
return descriptor.getDecoration(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(String query, TextView node) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(Object.class);
|
||||
return descriptor.matches(query, node);
|
||||
}
|
||||
}
|
||||
708
android/plugins/inspector/descriptors/ViewDescriptor.java
Normal file
708
android/plugins/inspector/descriptors/ViewDescriptor.java
Normal file
@@ -0,0 +1,708 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.facebook.sonar.plugins.inspector.descriptors;
|
||||
|
||||
import static com.facebook.sonar.plugins.inspector.InspectorValue.Type.Color;
|
||||
import static com.facebook.sonar.plugins.inspector.InspectorValue.Type.Enum;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.support.v4.view.MarginLayoutParamsCompat;
|
||||
import android.support.v4.view.ViewCompat;
|
||||
import android.util.SparseArray;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewGroup.LayoutParams;
|
||||
import android.view.ViewGroup.MarginLayoutParams;
|
||||
import android.view.ViewParent;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.LinearLayout;
|
||||
import com.facebook.sonar.core.SonarDynamic;
|
||||
import com.facebook.sonar.core.SonarObject;
|
||||
import com.facebook.sonar.plugins.inspector.HighlightedOverlay;
|
||||
import com.facebook.sonar.plugins.inspector.InspectorValue;
|
||||
import com.facebook.sonar.plugins.inspector.Named;
|
||||
import com.facebook.sonar.plugins.inspector.NodeDescriptor;
|
||||
import com.facebook.sonar.plugins.inspector.Touch;
|
||||
import com.facebook.sonar.plugins.inspector.descriptors.utils.AccessibilityUtil;
|
||||
import com.facebook.sonar.plugins.inspector.descriptors.utils.EnumMapping;
|
||||
import com.facebook.stetho.common.android.ResourcesUtil;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class ViewDescriptor extends NodeDescriptor<View> {
|
||||
|
||||
private static Field sKeyedTagsField;
|
||||
private static Field sListenerInfoField;
|
||||
private static Field sOnClickListenerField;
|
||||
|
||||
static {
|
||||
try {
|
||||
sKeyedTagsField = View.class.getDeclaredField("mKeyedTags");
|
||||
sKeyedTagsField.setAccessible(true);
|
||||
|
||||
sListenerInfoField = View.class.getDeclaredField("mListenerInfo");
|
||||
sListenerInfoField.setAccessible(true);
|
||||
|
||||
final String viewInfoClassName = View.class.getName() + "$ListenerInfo";
|
||||
sOnClickListenerField = Class.forName(viewInfoClassName).getDeclaredField("mOnClickListener");
|
||||
sOnClickListenerField.setAccessible(true);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(final View node) {}
|
||||
|
||||
@Override
|
||||
public String getId(View node) {
|
||||
return Integer.toString(System.identityHashCode(node));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName(View node) {
|
||||
return node.getClass().getSimpleName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getChildCount(View node) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Object getChildAt(View node, int index) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Named<SonarObject>> getData(View node) {
|
||||
final SonarObject.Builder viewProps =
|
||||
new SonarObject.Builder()
|
||||
.put("height", InspectorValue.mutable(node.getHeight()))
|
||||
.put("width", InspectorValue.mutable(node.getWidth()))
|
||||
.put("alpha", InspectorValue.mutable(node.getAlpha()))
|
||||
.put("visibility", sVisibilityMapping.get(node.getVisibility()))
|
||||
.put("background", fromDrawable(node.getBackground()))
|
||||
.put("tag", InspectorValue.mutable(node.getTag()))
|
||||
.put("keyedTags", getTags(node))
|
||||
.put("layoutParams", getLayoutParams(node))
|
||||
.put(
|
||||
"state",
|
||||
new SonarObject.Builder()
|
||||
.put("enabled", InspectorValue.mutable(node.isEnabled()))
|
||||
.put("activated", InspectorValue.mutable(node.isActivated()))
|
||||
.put("focused", node.isFocused())
|
||||
.put("selected", InspectorValue.mutable(node.isSelected())))
|
||||
.put(
|
||||
"bounds",
|
||||
new SonarObject.Builder()
|
||||
.put("left", InspectorValue.mutable(node.getLeft()))
|
||||
.put("right", InspectorValue.mutable(node.getRight()))
|
||||
.put("top", InspectorValue.mutable(node.getTop()))
|
||||
.put("bottom", InspectorValue.mutable(node.getBottom())))
|
||||
.put(
|
||||
"padding",
|
||||
new SonarObject.Builder()
|
||||
.put("left", InspectorValue.mutable(node.getPaddingLeft()))
|
||||
.put("top", InspectorValue.mutable(node.getPaddingTop()))
|
||||
.put("right", InspectorValue.mutable(node.getPaddingRight()))
|
||||
.put("bottom", InspectorValue.mutable(node.getPaddingBottom())))
|
||||
.put(
|
||||
"rotation",
|
||||
new SonarObject.Builder()
|
||||
.put("x", InspectorValue.mutable(node.getRotationX()))
|
||||
.put("y", InspectorValue.mutable(node.getRotationY()))
|
||||
.put("z", InspectorValue.mutable(node.getRotation())))
|
||||
.put(
|
||||
"scale",
|
||||
new SonarObject.Builder()
|
||||
.put("x", InspectorValue.mutable(node.getScaleX()))
|
||||
.put("y", InspectorValue.mutable(node.getScaleY())))
|
||||
.put(
|
||||
"pivot",
|
||||
new SonarObject.Builder()
|
||||
.put("x", InspectorValue.mutable(node.getPivotX()))
|
||||
.put("y", InspectorValue.mutable(node.getPivotY())));
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
viewProps
|
||||
.put("layoutDirection", sLayoutDirectionMapping.get(node.getLayoutDirection()))
|
||||
.put("textDirection", sTextDirectionMapping.get(node.getTextDirection()))
|
||||
.put("textAlignment", sTextAlignmentMapping.get(node.getTextAlignment()));
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
viewProps.put("elevation", InspectorValue.mutable(node.getElevation()));
|
||||
}
|
||||
|
||||
SonarObject.Builder translation =
|
||||
new SonarObject.Builder()
|
||||
.put("x", InspectorValue.mutable(node.getTranslationX()))
|
||||
.put("y", InspectorValue.mutable(node.getTranslationY()));
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
translation.put("z", InspectorValue.mutable(node.getTranslationZ()));
|
||||
}
|
||||
viewProps.put("translation", translation);
|
||||
|
||||
SonarObject.Builder position =
|
||||
new SonarObject.Builder()
|
||||
.put("x", InspectorValue.mutable(node.getX()))
|
||||
.put("y", InspectorValue.mutable(node.getY()));
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
|
||||
position.put("z", InspectorValue.mutable(node.getZ()));
|
||||
}
|
||||
viewProps.put("position", position);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
viewProps.put("foreground", fromDrawable(node.getForeground()));
|
||||
}
|
||||
|
||||
return Arrays.asList(
|
||||
new Named<>("View", viewProps.build()),
|
||||
new Named<>("Accessibility", getAccessibilityData(node)));
|
||||
}
|
||||
|
||||
private static SonarObject getAccessibilityData(View view) {
|
||||
final SonarObject.Builder accessibilityProps = new SonarObject.Builder();
|
||||
|
||||
// This needs to be an empty string to be mutable. See t20470623.
|
||||
CharSequence contentDescription =
|
||||
view.getContentDescription() != null ? view.getContentDescription() : "";
|
||||
accessibilityProps.put("content-description", InspectorValue.mutable(contentDescription));
|
||||
accessibilityProps.put("focusable", InspectorValue.mutable(view.isFocusable()));
|
||||
accessibilityProps.put("node-info", AccessibilityUtil.getAccessibilityNodeInfoProperties(view));
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||
accessibilityProps.put(
|
||||
"important-for-accessibility",
|
||||
AccessibilityUtil.sImportantForAccessibilityMapping.get(
|
||||
view.getImportantForAccessibility()));
|
||||
}
|
||||
|
||||
AccessibilityUtil.addTalkbackProperties(accessibilityProps, view);
|
||||
|
||||
return accessibilityProps.build();
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
|
||||
@Override
|
||||
public void setValue(View node, String[] path, SonarDynamic value) {
|
||||
if (path[0].equals("Accessibility")) {
|
||||
setAccessibilityValue(node, path, value);
|
||||
}
|
||||
|
||||
if (!path[0].equals("View")) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (path[1]) {
|
||||
case "elevation":
|
||||
node.setElevation(value.asFloat());
|
||||
break;
|
||||
case "alpha":
|
||||
node.setAlpha(value.asFloat());
|
||||
break;
|
||||
case "visibility":
|
||||
node.setVisibility(sVisibilityMapping.get(value.asString()));
|
||||
break;
|
||||
case "layoutParams":
|
||||
setLayoutParams(node, Arrays.copyOfRange(path, 1, path.length), value);
|
||||
break;
|
||||
case "layoutDirection":
|
||||
node.setLayoutDirection(sLayoutDirectionMapping.get(value.asString()));
|
||||
break;
|
||||
case "textDirection":
|
||||
node.setTextDirection(sTextDirectionMapping.get(value.asString()));
|
||||
break;
|
||||
case "textAlignment":
|
||||
node.setTextAlignment(sTextAlignmentMapping.get(value.asString()));
|
||||
break;
|
||||
case "background":
|
||||
node.setBackground(new ColorDrawable(value.asInt()));
|
||||
break;
|
||||
case "foreground":
|
||||
node.setForeground(new ColorDrawable(value.asInt()));
|
||||
break;
|
||||
case "state":
|
||||
switch (path[2]) {
|
||||
case "enabled":
|
||||
node.setEnabled(value.asBoolean());
|
||||
break;
|
||||
case "activated":
|
||||
node.setActivated(value.asBoolean());
|
||||
break;
|
||||
case "selected":
|
||||
node.setSelected(value.asBoolean());
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case "bounds":
|
||||
switch (path[2]) {
|
||||
case "left":
|
||||
node.setLeft(value.asInt());
|
||||
break;
|
||||
case "top":
|
||||
node.setTop(value.asInt());
|
||||
break;
|
||||
case "right":
|
||||
node.setRight(value.asInt());
|
||||
break;
|
||||
case "bottom":
|
||||
node.setBottom(value.asInt());
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case "padding":
|
||||
switch (path[2]) {
|
||||
case "left":
|
||||
node.setPadding(
|
||||
value.asInt(),
|
||||
node.getPaddingTop(),
|
||||
node.getPaddingRight(),
|
||||
node.getPaddingBottom());
|
||||
break;
|
||||
case "top":
|
||||
node.setPadding(
|
||||
node.getPaddingLeft(),
|
||||
value.asInt(),
|
||||
node.getPaddingRight(),
|
||||
node.getPaddingBottom());
|
||||
break;
|
||||
case "right":
|
||||
node.setPadding(
|
||||
node.getPaddingLeft(),
|
||||
node.getPaddingTop(),
|
||||
value.asInt(),
|
||||
node.getPaddingBottom());
|
||||
break;
|
||||
case "bottom":
|
||||
node.setPadding(
|
||||
node.getPaddingLeft(), node.getPaddingTop(), node.getPaddingRight(), value.asInt());
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case "rotation":
|
||||
switch (path[2]) {
|
||||
case "x":
|
||||
node.setRotationX(value.asFloat());
|
||||
break;
|
||||
case "y":
|
||||
node.setRotationY(value.asFloat());
|
||||
break;
|
||||
case "z":
|
||||
node.setRotation(value.asFloat());
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case "translation":
|
||||
switch (path[2]) {
|
||||
case "x":
|
||||
node.setTranslationX(value.asFloat());
|
||||
break;
|
||||
case "y":
|
||||
node.setTranslationY(value.asFloat());
|
||||
break;
|
||||
case "z":
|
||||
node.setTranslationZ(value.asFloat());
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case "position":
|
||||
switch (path[2]) {
|
||||
case "x":
|
||||
node.setX(value.asFloat());
|
||||
break;
|
||||
case "y":
|
||||
node.setY(value.asFloat());
|
||||
break;
|
||||
case "z":
|
||||
node.setZ(value.asFloat());
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case "scale":
|
||||
switch (path[2]) {
|
||||
case "x":
|
||||
node.setScaleX(value.asFloat());
|
||||
break;
|
||||
case "y":
|
||||
node.setScaleY(value.asFloat());
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case "pivot":
|
||||
switch (path[2]) {
|
||||
case "x":
|
||||
node.setPivotY(value.asFloat());
|
||||
break;
|
||||
case "y":
|
||||
node.setPivotX(value.asFloat());
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case "width":
|
||||
LayoutParams lpw = node.getLayoutParams();
|
||||
lpw.width = value.asInt();
|
||||
node.setLayoutParams(lpw);
|
||||
break;
|
||||
case "height":
|
||||
LayoutParams lph = node.getLayoutParams();
|
||||
lph.height = value.asInt();
|
||||
node.setLayoutParams(lph);
|
||||
break;
|
||||
}
|
||||
|
||||
invalidate(node);
|
||||
}
|
||||
|
||||
private void setAccessibilityValue(View node, String[] path, SonarDynamic value) {
|
||||
switch (path[1]) {
|
||||
case "focusable":
|
||||
node.setFocusable(value.asBoolean());
|
||||
break;
|
||||
case "important-for-accessibility":
|
||||
node.setImportantForAccessibility(
|
||||
AccessibilityUtil.sImportantForAccessibilityMapping.get(value.asString()));
|
||||
break;
|
||||
case "content-description":
|
||||
node.setContentDescription(value.asString());
|
||||
break;
|
||||
}
|
||||
invalidate(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Named<String>> getAttributes(View node) throws Exception {
|
||||
final List<Named<String>> attributes = new ArrayList<>();
|
||||
|
||||
final String resourceId = getResourceId(node);
|
||||
if (resourceId != null) {
|
||||
attributes.add(new Named<>("id", resourceId));
|
||||
}
|
||||
|
||||
if (sListenerInfoField != null && sOnClickListenerField != null) {
|
||||
final Object listenerInfo = sListenerInfoField.get(node);
|
||||
if (listenerInfo != null) {
|
||||
final OnClickListener clickListener =
|
||||
(OnClickListener) sOnClickListenerField.get(listenerInfo);
|
||||
if (clickListener != null) {
|
||||
attributes.add(new Named<>("onClick", clickListener.getClass().getName()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return attributes;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static String getResourceId(View node) {
|
||||
final int id = node.getId();
|
||||
|
||||
if (id == View.NO_ID) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ResourcesUtil.getIdStringQuietly(node.getContext(), node.getResources(), id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setHighlighted(View node, boolean selected) {
|
||||
// We need to figure out whether the given View has a parent View since margins are not
|
||||
// included within a View's bounds. So, in order to display the margin values for a particular
|
||||
// view, we need to apply an overlay on its parent rather than itself.
|
||||
final View targetView;
|
||||
final ViewParent parent = node.getParent();
|
||||
if (parent instanceof View) {
|
||||
targetView = (View) parent;
|
||||
} else {
|
||||
targetView = node;
|
||||
}
|
||||
|
||||
if (!selected) {
|
||||
HighlightedOverlay.removeHighlight(targetView);
|
||||
return;
|
||||
}
|
||||
|
||||
final Rect padding =
|
||||
new Rect(
|
||||
ViewCompat.getPaddingStart(node),
|
||||
node.getPaddingTop(),
|
||||
ViewCompat.getPaddingEnd(node),
|
||||
node.getPaddingBottom());
|
||||
|
||||
final Rect margin;
|
||||
final ViewGroup.LayoutParams params = node.getLayoutParams();
|
||||
if (params instanceof ViewGroup.MarginLayoutParams) {
|
||||
final ViewGroup.MarginLayoutParams marginParams = (ViewGroup.MarginLayoutParams) params;
|
||||
margin =
|
||||
new Rect(
|
||||
MarginLayoutParamsCompat.getMarginStart(marginParams),
|
||||
marginParams.topMargin,
|
||||
MarginLayoutParamsCompat.getMarginEnd(marginParams),
|
||||
marginParams.bottomMargin);
|
||||
} else {
|
||||
margin = new Rect();
|
||||
}
|
||||
|
||||
final int left = node.getLeft();
|
||||
final int top = node.getTop();
|
||||
final Rect contentBounds = new Rect(left, top, left + node.getWidth(), top + node.getHeight());
|
||||
if (targetView == node) {
|
||||
// If the View doesn't have a parent View that we're applying the overlay to, then
|
||||
// we need to ensure that it is aligned to 0, 0, rather than its relative location to its
|
||||
// parent
|
||||
contentBounds.offset(-left, -top);
|
||||
}
|
||||
|
||||
HighlightedOverlay.setHighlighted(targetView, margin, padding, contentBounds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hitTest(View node, Touch touch) {
|
||||
touch.finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable String getDecoration(View obj) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(String query, View node) throws Exception {
|
||||
final String resourceId = getResourceId(node);
|
||||
|
||||
if (resourceId != null && resourceId.toLowerCase().contains(query)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final NodeDescriptor objectDescriptor = descriptorForClass(Object.class);
|
||||
return objectDescriptor.matches(query, node);
|
||||
}
|
||||
|
||||
private SonarObject getTags(final View node) {
|
||||
final SonarObject.Builder tags = new SonarObject.Builder();
|
||||
if (sKeyedTagsField == null) {
|
||||
return tags.build();
|
||||
}
|
||||
|
||||
new ErrorReportingRunnable() {
|
||||
@Override
|
||||
protected void runOrThrow() throws Exception {
|
||||
final SparseArray keyedTags = (SparseArray) sKeyedTagsField.get(node);
|
||||
if (keyedTags != null) {
|
||||
for (int i = 0, count = keyedTags.size(); i < count; i++) {
|
||||
final String id =
|
||||
ResourcesUtil.getIdStringQuietly(
|
||||
node.getContext(), node.getResources(), keyedTags.keyAt(i));
|
||||
tags.put(id, keyedTags.valueAt(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
}.run();
|
||||
|
||||
return tags.build();
|
||||
}
|
||||
|
||||
private static InspectorValue fromDrawable(Drawable d) {
|
||||
if (d instanceof ColorDrawable) {
|
||||
return InspectorValue.mutable(Color, ((ColorDrawable) d).getColor());
|
||||
}
|
||||
return InspectorValue.mutable(Color, 0);
|
||||
}
|
||||
|
||||
private static SonarObject getLayoutParams(View node) {
|
||||
final LayoutParams layoutParams = node.getLayoutParams();
|
||||
final SonarObject.Builder params = new SonarObject.Builder();
|
||||
|
||||
params.put("width", fromSize(layoutParams.width));
|
||||
params.put("height", fromSize(layoutParams.height));
|
||||
|
||||
if (layoutParams instanceof MarginLayoutParams) {
|
||||
final MarginLayoutParams marginLayoutParams = (MarginLayoutParams) layoutParams;
|
||||
params.put(
|
||||
"margin",
|
||||
new SonarObject.Builder()
|
||||
.put("left", InspectorValue.mutable(marginLayoutParams.leftMargin))
|
||||
.put("top", InspectorValue.mutable(marginLayoutParams.topMargin))
|
||||
.put("right", InspectorValue.mutable(marginLayoutParams.rightMargin))
|
||||
.put("bottom", InspectorValue.mutable(marginLayoutParams.bottomMargin)));
|
||||
}
|
||||
|
||||
if (layoutParams instanceof FrameLayout.LayoutParams) {
|
||||
final FrameLayout.LayoutParams frameLayoutParams = (FrameLayout.LayoutParams) layoutParams;
|
||||
params.put("gravity", sGravityMapping.get(frameLayoutParams.gravity));
|
||||
}
|
||||
|
||||
if (layoutParams instanceof LinearLayout.LayoutParams) {
|
||||
final LinearLayout.LayoutParams linearLayoutParams = (LinearLayout.LayoutParams) layoutParams;
|
||||
params
|
||||
.put("weight", InspectorValue.mutable(linearLayoutParams.weight))
|
||||
.put("gravity", sGravityMapping.get(linearLayoutParams.gravity));
|
||||
}
|
||||
|
||||
return params.build();
|
||||
}
|
||||
|
||||
private void setLayoutParams(View node, String[] path, SonarDynamic value) {
|
||||
final LayoutParams params = node.getLayoutParams();
|
||||
|
||||
switch (path[0]) {
|
||||
case "width":
|
||||
params.width = toSize(value.asString());
|
||||
break;
|
||||
case "height":
|
||||
params.height = toSize(value.asString());
|
||||
break;
|
||||
case "weight":
|
||||
final LinearLayout.LayoutParams linearParams = (LinearLayout.LayoutParams) params;
|
||||
linearParams.weight = value.asFloat();
|
||||
break;
|
||||
}
|
||||
|
||||
if (params instanceof MarginLayoutParams) {
|
||||
final MarginLayoutParams marginParams = (MarginLayoutParams) params;
|
||||
|
||||
switch (path[0]) {
|
||||
case "margin":
|
||||
switch (path[1]) {
|
||||
case "left":
|
||||
marginParams.leftMargin = value.asInt();
|
||||
break;
|
||||
case "top":
|
||||
marginParams.topMargin = value.asInt();
|
||||
break;
|
||||
case "right":
|
||||
marginParams.rightMargin = value.asInt();
|
||||
break;
|
||||
case "bottom":
|
||||
marginParams.bottomMargin = value.asInt();
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (params instanceof FrameLayout.LayoutParams) {
|
||||
final FrameLayout.LayoutParams frameLayoutParams = (FrameLayout.LayoutParams) params;
|
||||
|
||||
switch (path[0]) {
|
||||
case "gravity":
|
||||
frameLayoutParams.gravity = sGravityMapping.get(value.asString());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (params instanceof LinearLayout.LayoutParams) {
|
||||
final LinearLayout.LayoutParams linearParams = (LinearLayout.LayoutParams) params;
|
||||
|
||||
switch (path[0]) {
|
||||
case "weight":
|
||||
linearParams.weight = value.asFloat();
|
||||
break;
|
||||
case "gravity":
|
||||
linearParams.gravity = sGravityMapping.get(value.asString());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
node.setLayoutParams(params);
|
||||
}
|
||||
|
||||
private static InspectorValue fromSize(int size) {
|
||||
switch (size) {
|
||||
case LayoutParams.WRAP_CONTENT:
|
||||
return InspectorValue.mutable(Enum, "WRAP_CONTENT");
|
||||
case LayoutParams.MATCH_PARENT:
|
||||
return InspectorValue.mutable(Enum, "MATCH_PARENT");
|
||||
default:
|
||||
return InspectorValue.mutable(Enum, Integer.toString(size));
|
||||
}
|
||||
}
|
||||
|
||||
private static int toSize(String size) {
|
||||
switch (size) {
|
||||
case "WRAP_CONTENT":
|
||||
return LayoutParams.WRAP_CONTENT;
|
||||
case "MATCH_PARENT":
|
||||
return LayoutParams.MATCH_PARENT;
|
||||
default:
|
||||
return Integer.parseInt(size);
|
||||
}
|
||||
}
|
||||
|
||||
private static final EnumMapping sVisibilityMapping =
|
||||
new EnumMapping("VISIBLE") {
|
||||
{
|
||||
put("VISIBLE", View.VISIBLE);
|
||||
put("INVISIBLE", View.INVISIBLE);
|
||||
put("GONE", View.GONE);
|
||||
}
|
||||
};
|
||||
|
||||
private static final EnumMapping sLayoutDirectionMapping =
|
||||
new EnumMapping("LAYOUT_DIRECTION_INHERIT") {
|
||||
{
|
||||
put("LAYOUT_DIRECTION_INHERIT", View.LAYOUT_DIRECTION_INHERIT);
|
||||
put("LAYOUT_DIRECTION_LOCALE", View.LAYOUT_DIRECTION_LOCALE);
|
||||
put("LAYOUT_DIRECTION_LTR", View.LAYOUT_DIRECTION_LTR);
|
||||
put("LAYOUT_DIRECTION_RTL", View.LAYOUT_DIRECTION_RTL);
|
||||
}
|
||||
};
|
||||
|
||||
private static final EnumMapping sTextDirectionMapping =
|
||||
new EnumMapping("TEXT_DIRECTION_INHERIT") {
|
||||
{
|
||||
put("TEXT_DIRECTION_INHERIT", View.TEXT_DIRECTION_INHERIT);
|
||||
put("TEXT_DIRECTION_FIRST_STRONG", View.TEXT_DIRECTION_FIRST_STRONG);
|
||||
put("TEXT_DIRECTION_ANY_RTL", View.TEXT_DIRECTION_ANY_RTL);
|
||||
put("TEXT_DIRECTION_LTR", View.TEXT_DIRECTION_LTR);
|
||||
put("TEXT_DIRECTION_RTL", View.TEXT_DIRECTION_RTL);
|
||||
put("TEXT_DIRECTION_LOCALE", View.TEXT_DIRECTION_LOCALE);
|
||||
put("TEXT_DIRECTION_FIRST_STRONG_LTR", View.TEXT_DIRECTION_FIRST_STRONG_LTR);
|
||||
put("TEXT_DIRECTION_FIRST_STRONG_RTL", View.TEXT_DIRECTION_FIRST_STRONG_RTL);
|
||||
}
|
||||
};
|
||||
|
||||
private static final EnumMapping sTextAlignmentMapping =
|
||||
new EnumMapping("TEXT_ALIGNMENT_INHERIT") {
|
||||
{
|
||||
put("TEXT_ALIGNMENT_INHERIT", View.TEXT_ALIGNMENT_INHERIT);
|
||||
put("TEXT_ALIGNMENT_GRAVITY", View.TEXT_ALIGNMENT_GRAVITY);
|
||||
put("TEXT_ALIGNMENT_TEXT_START", View.TEXT_ALIGNMENT_TEXT_START);
|
||||
put("TEXT_ALIGNMENT_TEXT_END", View.TEXT_ALIGNMENT_TEXT_END);
|
||||
put("TEXT_ALIGNMENT_CENTER", View.TEXT_ALIGNMENT_CENTER);
|
||||
put("TEXT_ALIGNMENT_VIEW_START", View.TEXT_ALIGNMENT_VIEW_START);
|
||||
put("TEXT_ALIGNMENT_VIEW_END", View.TEXT_ALIGNMENT_VIEW_END);
|
||||
}
|
||||
};
|
||||
|
||||
private static final EnumMapping sGravityMapping =
|
||||
new EnumMapping("NO_GRAVITY") {
|
||||
{
|
||||
put("NO_GRAVITY", Gravity.NO_GRAVITY);
|
||||
put("LEFT", Gravity.LEFT);
|
||||
put("TOP", Gravity.TOP);
|
||||
put("RIGHT", Gravity.RIGHT);
|
||||
put("BOTTOM", Gravity.BOTTOM);
|
||||
put("CENTER", Gravity.CENTER);
|
||||
put("CENTER_VERTICAL", Gravity.CENTER_VERTICAL);
|
||||
put("FILL_VERTICAL", Gravity.FILL_VERTICAL);
|
||||
put("CENTER_HORIZONTAL", Gravity.CENTER_HORIZONTAL);
|
||||
put("FILL_HORIZONTAL", Gravity.FILL_HORIZONTAL);
|
||||
}
|
||||
};
|
||||
}
|
||||
273
android/plugins/inspector/descriptors/ViewGroupDescriptor.java
Normal file
273
android/plugins/inspector/descriptors/ViewGroupDescriptor.java
Normal file
@@ -0,0 +1,273 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.facebook.sonar.plugins.inspector.descriptors;
|
||||
|
||||
import static android.support.v4.view.ViewGroupCompat.LAYOUT_MODE_CLIP_BOUNDS;
|
||||
import static android.support.v4.view.ViewGroupCompat.LAYOUT_MODE_OPTICAL_BOUNDS;
|
||||
import static com.facebook.sonar.plugins.inspector.InspectorValue.Type.Boolean;
|
||||
import static com.facebook.sonar.plugins.inspector.InspectorValue.Type.Enum;
|
||||
|
||||
import android.os.Build;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import com.facebook.sonar.R;
|
||||
import com.facebook.sonar.core.SonarDynamic;
|
||||
import com.facebook.sonar.core.SonarObject;
|
||||
import com.facebook.sonar.plugins.inspector.HiddenNode;
|
||||
import com.facebook.sonar.plugins.inspector.InspectorValue;
|
||||
import com.facebook.sonar.plugins.inspector.Named;
|
||||
import com.facebook.sonar.plugins.inspector.NodeDescriptor;
|
||||
import com.facebook.sonar.plugins.inspector.Touch;
|
||||
import com.facebook.stetho.common.android.FragmentCompatUtil;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class ViewGroupDescriptor extends NodeDescriptor<ViewGroup> {
|
||||
|
||||
private class NodeKey {
|
||||
private int[] mKey;
|
||||
|
||||
boolean set(ViewGroup node) {
|
||||
final int childCount = node.getChildCount();
|
||||
final int[] key = new int[childCount];
|
||||
|
||||
for (int i = 0; i < childCount; i++) {
|
||||
final View child = node.getChildAt(i);
|
||||
key[i] = System.identityHashCode(child);
|
||||
}
|
||||
|
||||
boolean changed = false;
|
||||
if (mKey == null) {
|
||||
changed = true;
|
||||
} else if (mKey.length != key.length) {
|
||||
changed = true;
|
||||
} else {
|
||||
for (int i = 0; i < childCount; i++) {
|
||||
if (mKey[i] != key[i]) {
|
||||
changed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mKey = key;
|
||||
return changed;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(final ViewGroup node) {
|
||||
final NodeKey key = new NodeKey();
|
||||
|
||||
final Runnable maybeInvalidate =
|
||||
new ErrorReportingRunnable() {
|
||||
@Override
|
||||
public void runOrThrow() throws Exception {
|
||||
if (connected()) {
|
||||
if (key.set(node)) {
|
||||
invalidate(node);
|
||||
}
|
||||
|
||||
final boolean hasAttachedToWindow =
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
|
||||
if (!hasAttachedToWindow || node.isAttachedToWindow()) {
|
||||
node.postDelayed(this, 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
node.postDelayed(maybeInvalidate, 1000);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId(ViewGroup node) throws Exception {
|
||||
NodeDescriptor descriptor = descriptorForClass(View.class);
|
||||
return descriptor.getId(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName(ViewGroup node) throws Exception {
|
||||
NodeDescriptor descriptor = descriptorForClass(View.class);
|
||||
return descriptor.getName(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getChildCount(ViewGroup node) {
|
||||
int childCount = 0;
|
||||
for (int i = 0, count = node.getChildCount(); i < count; i++) {
|
||||
if (!(node.getChildAt(i) instanceof HiddenNode)) {
|
||||
childCount++;
|
||||
}
|
||||
}
|
||||
return childCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Object getChildAt(ViewGroup node, int index) {
|
||||
for (int i = 0, count = node.getChildCount(); i < count; i++) {
|
||||
final View child = node.getChildAt(i);
|
||||
if (child instanceof HiddenNode) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (i >= index) {
|
||||
final Object fragment = getAttachedFragmentForView(child);
|
||||
if (fragment != null && !FragmentCompatUtil.isDialogFragment(fragment)) {
|
||||
return fragment;
|
||||
}
|
||||
|
||||
return child;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Named<SonarObject>> getData(ViewGroup node) throws Exception {
|
||||
final List<Named<SonarObject>> props = new ArrayList<>();
|
||||
final NodeDescriptor descriptor = descriptorForClass(View.class);
|
||||
|
||||
final SonarObject.Builder vgProps = new SonarObject.Builder();
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
||||
vgProps
|
||||
.put(
|
||||
"layoutMode",
|
||||
InspectorValue.mutable(
|
||||
Enum,
|
||||
node.getLayoutMode() == LAYOUT_MODE_CLIP_BOUNDS
|
||||
? "LAYOUT_MODE_CLIP_BOUNDS"
|
||||
: "LAYOUT_MODE_OPTICAL_BOUNDS"))
|
||||
.put("clipChildren", InspectorValue.mutable(Boolean, node.getClipChildren()));
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
vgProps.put("clipToPadding", InspectorValue.mutable(Boolean, node.getClipToPadding()));
|
||||
}
|
||||
|
||||
props.add(0, new Named<>("ViewGroup", vgProps.build()));
|
||||
|
||||
props.addAll(descriptor.getData(node));
|
||||
|
||||
return props;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(ViewGroup node, String[] path, SonarDynamic value) throws Exception {
|
||||
switch (path[0]) {
|
||||
case "ViewGroup":
|
||||
switch (path[1]) {
|
||||
case "layoutMode":
|
||||
switch (value.asString()) {
|
||||
case "LAYOUT_MODE_CLIP_BOUNDS":
|
||||
node.setLayoutMode(LAYOUT_MODE_CLIP_BOUNDS);
|
||||
break;
|
||||
case "LAYOUT_MODE_OPTICAL_BOUNDS":
|
||||
node.setLayoutMode(LAYOUT_MODE_OPTICAL_BOUNDS);
|
||||
break;
|
||||
default:
|
||||
node.setLayoutMode(-1);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case "clipChildren":
|
||||
node.setClipChildren(value.asBoolean());
|
||||
break;
|
||||
case "clipToPadding":
|
||||
node.setClipToPadding(value.asBoolean());
|
||||
break;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
final NodeDescriptor descriptor = descriptorForClass(View.class);
|
||||
descriptor.setValue(node, path, value);
|
||||
break;
|
||||
}
|
||||
invalidate(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Named<String>> getAttributes(ViewGroup node) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(View.class);
|
||||
return descriptor.getAttributes(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setHighlighted(ViewGroup node, boolean selected) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(View.class);
|
||||
descriptor.setHighlighted(node, selected);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hitTest(ViewGroup node, Touch touch) {
|
||||
for (int i = node.getChildCount() - 1; i >= 0; i--) {
|
||||
final View child = node.getChildAt(i);
|
||||
if (child instanceof HiddenNode
|
||||
|| child.getVisibility() != View.VISIBLE
|
||||
|| shouldSkip(child)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final int scrollX = node.getScrollX();
|
||||
final int scrollY = node.getScrollY();
|
||||
|
||||
final int left = (child.getLeft() + (int) child.getTranslationX()) - scrollX;
|
||||
final int top = (child.getTop() + (int) child.getTranslationY()) - scrollY;
|
||||
final int right = (child.getRight() + (int) child.getTranslationX()) - scrollX;
|
||||
final int bottom = (child.getBottom() + (int) child.getTranslationY()) - scrollY;
|
||||
|
||||
final boolean hit = touch.containedIn(left, top, right, bottom);
|
||||
|
||||
if (hit) {
|
||||
touch.continueWithOffset(i, left, top);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
touch.finish();
|
||||
}
|
||||
|
||||
private static boolean shouldSkip(View view) {
|
||||
Object tag = view.getTag(R.id.sonar_skip_view_traversal);
|
||||
if (!(tag instanceof Boolean)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (Boolean) tag;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable String getDecoration(ViewGroup obj) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(String query, ViewGroup node) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(Object.class);
|
||||
return descriptor.matches(query, node);
|
||||
}
|
||||
|
||||
private static Object getAttachedFragmentForView(View v) {
|
||||
try {
|
||||
final Object fragment = FragmentCompatUtil.findFragmentForView(v);
|
||||
boolean added = false;
|
||||
if (fragment instanceof android.app.Fragment) {
|
||||
added = ((android.app.Fragment) fragment).isAdded();
|
||||
} else if (fragment instanceof android.support.v4.app.Fragment) {
|
||||
added = ((android.support.v4.app.Fragment) fragment).isAdded();
|
||||
}
|
||||
|
||||
return added ? fragment : null;
|
||||
} catch (RuntimeException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
81
android/plugins/inspector/descriptors/WindowDescriptor.java
Normal file
81
android/plugins/inspector/descriptors/WindowDescriptor.java
Normal file
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.facebook.sonar.plugins.inspector.descriptors;
|
||||
|
||||
import android.view.View;
|
||||
import android.view.Window;
|
||||
import com.facebook.sonar.core.SonarDynamic;
|
||||
import com.facebook.sonar.core.SonarObject;
|
||||
import com.facebook.sonar.plugins.inspector.Named;
|
||||
import com.facebook.sonar.plugins.inspector.NodeDescriptor;
|
||||
import com.facebook.sonar.plugins.inspector.Touch;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class WindowDescriptor extends NodeDescriptor<Window> {
|
||||
|
||||
@Override
|
||||
public void init(Window node) {}
|
||||
|
||||
@Override
|
||||
public String getId(Window node) {
|
||||
return Integer.toString(System.identityHashCode(node));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName(Window node) {
|
||||
return node.getClass().getSimpleName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getChildCount(Window node) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getChildAt(Window node, int index) {
|
||||
return node.getDecorView();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Named<SonarObject>> getData(Window node) {
|
||||
return Collections.EMPTY_LIST;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(Window node, String[] path, SonarDynamic value) {}
|
||||
|
||||
@Override
|
||||
public List<Named<String>> getAttributes(Window node) {
|
||||
return Collections.EMPTY_LIST;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setHighlighted(Window node, boolean selected) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(View.class);
|
||||
descriptor.setHighlighted(node.getDecorView(), selected);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hitTest(Window node, Touch touch) {
|
||||
touch.continueWithOffset(0, 0, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable String getDecoration(Window obj) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(String query, Window node) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(Object.class);
|
||||
return descriptor.matches(query, node);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,383 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
package com.facebook.sonar.plugins.inspector.descriptors.utils;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.support.v4.view.ViewCompat;
|
||||
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewParent;
|
||||
import com.facebook.sonar.plugins.inspector.descriptors.utils.AccessibilityRoleUtil.AccessibilityRole;
|
||||
import java.util.List;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* This class provides utility methods for determining certain accessibility properties of {@link
|
||||
* View}s and {@link AccessibilityNodeInfoCompat}s. It is porting some of the checks from {@link
|
||||
* com.googlecode.eyesfree.utils.AccessibilityNodeInfoUtils}, but has stripped many features which
|
||||
* are unnecessary here.
|
||||
*/
|
||||
public class AccessibilityEvaluationUtil {
|
||||
|
||||
private AccessibilityEvaluationUtil() {}
|
||||
|
||||
/**
|
||||
* Returns whether the specified node has text or a content description.
|
||||
*
|
||||
* @param node The node to check.
|
||||
* @return {@code true} if the node has text.
|
||||
*/
|
||||
public static boolean hasText(@Nullable AccessibilityNodeInfoCompat node) {
|
||||
return node != null
|
||||
&& node.getCollectionInfo() == null
|
||||
&& (!TextUtils.isEmpty(node.getText()) || !TextUtils.isEmpty(node.getContentDescription()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the supplied {@link View} and {@link AccessibilityNodeInfoCompat} would produce
|
||||
* spoken feedback if it were accessibility focused. NOTE: not all speaking nodes are focusable.
|
||||
*
|
||||
* @param view The {@link View} to evaluate
|
||||
* @param node The {@link AccessibilityNodeInfoCompat} to evaluate
|
||||
* @return {@code true} if it meets the criterion for producing spoken feedback
|
||||
*/
|
||||
public static boolean isSpeakingNode(
|
||||
@Nullable AccessibilityNodeInfoCompat node, @Nullable View view) {
|
||||
if (node == null || view == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final int important = ViewCompat.getImportantForAccessibility(view);
|
||||
if (important == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
|
||||
|| (important == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO && node.getChildCount() <= 0)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return node.isCheckable() || hasText(node) || hasNonActionableSpeakingDescendants(node, view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the supplied {@link View} and {@link AccessibilityNodeInfoCompat} has any
|
||||
* children which are not independently accessibility focusable and also have a spoken
|
||||
* description.
|
||||
*
|
||||
* <p>NOTE: Accessibility services will include these children's descriptions in the closest
|
||||
* focusable ancestor.
|
||||
*
|
||||
* @param view The {@link View} to evaluate
|
||||
* @param node The {@link AccessibilityNodeInfoCompat} to evaluate
|
||||
* @return {@code true} if it has any non-actionable speaking descendants within its subtree
|
||||
*/
|
||||
public static boolean hasNonActionableSpeakingDescendants(
|
||||
@Nullable AccessibilityNodeInfoCompat node, @Nullable View view) {
|
||||
|
||||
if (node == null || view == null || !(view instanceof ViewGroup)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final ViewGroup viewGroup = (ViewGroup) view;
|
||||
for (int i = 0, count = viewGroup.getChildCount(); i < count; i++) {
|
||||
final View childView = viewGroup.getChildAt(i);
|
||||
|
||||
if (childView == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final AccessibilityNodeInfoCompat childNode = AccessibilityNodeInfoCompat.obtain();
|
||||
try {
|
||||
ViewCompat.onInitializeAccessibilityNodeInfo(childView, childNode);
|
||||
|
||||
if (!node.isVisibleToUser()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isAccessibilityFocusable(childNode, childView)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isSpeakingNode(childNode, childView)) {
|
||||
return true;
|
||||
}
|
||||
} finally {
|
||||
if (childNode != null) {
|
||||
childNode.recycle();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the provided {@link View} and {@link AccessibilityNodeInfoCompat} meet the
|
||||
* criteria for gaining accessibility focus.
|
||||
*
|
||||
* <p>Note: this is evaluating general focusability by accessibility services, and does not mean
|
||||
* this view will be guaranteed to be focused by specific services such as Talkback. For Talkback
|
||||
* focusability, see {@link #isTalkbackFocusable(View)}
|
||||
*
|
||||
* @param view The {@link View} to evaluate
|
||||
* @param node The {@link AccessibilityNodeInfoCompat} to evaluate
|
||||
* @return {@code true} if it is possible to gain accessibility focus
|
||||
*/
|
||||
public static boolean isAccessibilityFocusable(
|
||||
@Nullable AccessibilityNodeInfoCompat node, @Nullable View view) {
|
||||
if (node == null || view == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Never focus invisible nodes.
|
||||
if (!node.isVisibleToUser()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Always focus "actionable" nodes.
|
||||
if (isActionableForAccessibility(node)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// only focus top-level list items with non-actionable speaking children.
|
||||
return isTopLevelScrollItem(node, view) && isSpeakingNode(node, view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the provided {@link View} and {@link AccessibilityNodeInfoCompat} is a
|
||||
* top-level item in a scrollable container.
|
||||
*
|
||||
* @param view The {@link View} to evaluate
|
||||
* @param node The {@link AccessibilityNodeInfoCompat} to evaluate
|
||||
* @return {@code true} if it is a top-level item in a scrollable container.
|
||||
*/
|
||||
public static boolean isTopLevelScrollItem(
|
||||
@Nullable AccessibilityNodeInfoCompat node, @Nullable View view) {
|
||||
if (node == null || view == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final View parent = (View) ViewCompat.getParentForAccessibility(view);
|
||||
if (parent == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (node.isScrollable()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final List actionList = node.getActionList();
|
||||
if (actionList.contains(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD)
|
||||
|| actionList.contains(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Top-level items in a scrolling pager are actually two levels down since the first
|
||||
// level items in pagers are the pages themselves.
|
||||
View grandparent = (View) ViewCompat.getParentForAccessibility(parent);
|
||||
if (grandparent != null
|
||||
&& AccessibilityRoleUtil.getRole(grandparent) == AccessibilityRole.PAGER) {
|
||||
return true;
|
||||
}
|
||||
|
||||
AccessibilityRole parentRole = AccessibilityRoleUtil.getRole(parent);
|
||||
return parentRole == AccessibilityRole.LIST
|
||||
|| parentRole == AccessibilityRole.GRID
|
||||
|| parentRole == AccessibilityRole.SCROLL_VIEW
|
||||
|| parentRole == AccessibilityRole.HORIZONTAL_SCROLL_VIEW;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a node is actionable. That is, the node supports one of {@link
|
||||
* AccessibilityNodeInfoCompat#isClickable()}, {@link AccessibilityNodeInfoCompat#isFocusable()},
|
||||
* or {@link AccessibilityNodeInfoCompat#isLongClickable()}.
|
||||
*
|
||||
* @param node The {@link AccessibilityNodeInfoCompat} to evaluate
|
||||
* @return {@code true} if node is actionable.
|
||||
*/
|
||||
public static boolean isActionableForAccessibility(@Nullable AccessibilityNodeInfoCompat node) {
|
||||
if (node == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (node.isClickable() || node.isLongClickable() || node.isFocusable()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final List actionList = node.getActionList();
|
||||
return actionList.contains(AccessibilityNodeInfoCompat.ACTION_CLICK)
|
||||
|| actionList.contains(AccessibilityNodeInfoCompat.ACTION_LONG_CLICK)
|
||||
|| actionList.contains(AccessibilityNodeInfoCompat.ACTION_FOCUS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if any of the provided {@link View}'s and {@link AccessibilityNodeInfoCompat}'s
|
||||
* ancestors can receive accessibility focus
|
||||
*
|
||||
* @param view The {@link View} to evaluate
|
||||
* @param node The {@link AccessibilityNodeInfoCompat} to evaluate
|
||||
* @return {@code true} if an ancestor of may receive accessibility focus
|
||||
*/
|
||||
public static boolean hasFocusableAncestor(
|
||||
@Nullable AccessibilityNodeInfoCompat node, @Nullable View view) {
|
||||
if (node == null || view == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final ViewParent parentView = ViewCompat.getParentForAccessibility(view);
|
||||
if (!(parentView instanceof View)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final AccessibilityNodeInfoCompat parentNode = AccessibilityNodeInfoCompat.obtain();
|
||||
try {
|
||||
ViewCompat.onInitializeAccessibilityNodeInfo((View) parentView, parentNode);
|
||||
if (parentNode == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (hasEqualBoundsToViewRoot(parentNode, (View) parentView)
|
||||
&& parentNode.getChildCount() > 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isAccessibilityFocusable(parentNode, (View) parentView)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (hasFocusableAncestor(parentNode, (View) parentView)) {
|
||||
return true;
|
||||
}
|
||||
} finally {
|
||||
parentNode.recycle();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a one given view is a descendant of another.
|
||||
*
|
||||
* @param view The {@link View} to evaluate
|
||||
* @param potentialAncestor The potential ancestor {@link View}
|
||||
* @return {@code true} if view is a descendant of potentialAncestor
|
||||
*/
|
||||
private static boolean viewIsDescendant(View view, View potentialAncestor) {
|
||||
ViewParent parent = view.getParent();
|
||||
while (parent != null) {
|
||||
if (parent == potentialAncestor) {
|
||||
return true;
|
||||
}
|
||||
parent = parent.getParent();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a View has the same size and position as its View Root.
|
||||
*
|
||||
* @param view The {@link View} to evaluate
|
||||
* @return {@code true} if view has equal bounds
|
||||
*/
|
||||
public static boolean hasEqualBoundsToViewRoot(AccessibilityNodeInfoCompat node, View view) {
|
||||
AndroidRootResolver rootResolver = new AndroidRootResolver();
|
||||
List<AndroidRootResolver.Root> roots = rootResolver.listActiveRoots();
|
||||
if (roots != null) {
|
||||
for (AndroidRootResolver.Root root : roots) {
|
||||
if (view == root.view) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (viewIsDescendant(view, root.view)) {
|
||||
Rect nodeBounds = new Rect();
|
||||
node.getBoundsInScreen(nodeBounds);
|
||||
|
||||
Rect viewRootBounds = new Rect();
|
||||
viewRootBounds.set(
|
||||
root.param.x,
|
||||
root.param.y,
|
||||
root.param.width + root.param.x,
|
||||
root.param.height + root.param.y);
|
||||
|
||||
return nodeBounds.equals(viewRootBounds);
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a given {@link View} will be focusable by Google's TalkBack screen reader.
|
||||
*
|
||||
* @param view The {@link View} to evaluate.
|
||||
* @return {@code boolean} if the view will be ignored by TalkBack.
|
||||
*/
|
||||
public static boolean isTalkbackFocusable(View view) {
|
||||
if (view == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final int important = ViewCompat.getImportantForAccessibility(view);
|
||||
if (important == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO
|
||||
|| important == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Go all the way up the tree to make sure no parent has hidden its descendants
|
||||
ViewParent parent = view.getParent();
|
||||
while (parent instanceof View) {
|
||||
if (ViewCompat.getImportantForAccessibility((View) parent)
|
||||
== ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS) {
|
||||
return false;
|
||||
}
|
||||
parent = parent.getParent();
|
||||
}
|
||||
|
||||
final AccessibilityNodeInfoCompat node = ViewAccessibilityHelper.createNodeInfoFromView(view);
|
||||
if (node == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Non-leaf nodes identical in size to their View Root should not be focusable.
|
||||
if (hasEqualBoundsToViewRoot(node, view) && node.getChildCount() > 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!node.isVisibleToUser()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isAccessibilityFocusable(node, view)) {
|
||||
if (node.getChildCount() <= 0) {
|
||||
// Leaves that are accessibility focusable are never ignored, even if they don't have a
|
||||
// speakable description
|
||||
return true;
|
||||
} else if (isSpeakingNode(node, view)) {
|
||||
// Node is focusable and has something to speak
|
||||
return true;
|
||||
}
|
||||
|
||||
// Node is focusable and has nothing to speak
|
||||
return false;
|
||||
}
|
||||
|
||||
// if view is not accessibility focusable, it needs to have text and no focusable ancestors.
|
||||
if (!hasText(node)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!hasFocusableAncestor(node, view)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} finally {
|
||||
node.recycle();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.facebook.sonar.plugins.inspector.descriptors.utils;
|
||||
|
||||
import android.support.v4.view.ViewCompat;
|
||||
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
|
||||
import android.view.View;
|
||||
import android.view.accessibility.AccessibilityNodeInfo;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Utility class that handles the addition of a "role" for accessibility to either a View or
|
||||
* AccessibilityNodeInfo.
|
||||
*/
|
||||
public class AccessibilityRoleUtil {
|
||||
|
||||
/**
|
||||
* These roles are defined by Google's TalkBack screen reader, and this list should be kept up to
|
||||
* date with their implementation. Details can be seen in their source code here:
|
||||
*
|
||||
* <p>https://github.com/google/talkback/blob/master/utils/src/main/java/Role.java
|
||||
*/
|
||||
public enum AccessibilityRole {
|
||||
NONE(null),
|
||||
BUTTON("android.widget.Button"),
|
||||
CHECK_BOX("android.widget.CompoundButton"),
|
||||
DROP_DOWN_LIST("android.widget.Spinner"),
|
||||
EDIT_TEXT("android.widget.EditText"),
|
||||
GRID("android.widget.GridView"),
|
||||
IMAGE("android.widget.ImageView"),
|
||||
IMAGE_BUTTON("android.widget.ImageView"),
|
||||
LIST("android.widget.AbsListView"),
|
||||
PAGER("android.support.v4.view.ViewPager"),
|
||||
RADIO_BUTTON("android.widget.RadioButton"),
|
||||
SEEK_CONTROL("android.widget.SeekBar"),
|
||||
SWITCH("android.widget.Switch"),
|
||||
TAB_BAR("android.widget.TabWidget"),
|
||||
TOGGLE_BUTTON("android.widget.ToggleButton"),
|
||||
VIEW_GROUP("android.view.ViewGroup"),
|
||||
WEB_VIEW("android.webkit.WebView"),
|
||||
CHECKED_TEXT_VIEW("android.widget.CheckedTextView"),
|
||||
PROGRESS_BAR("android.widget.ProgressBar"),
|
||||
ACTION_BAR_TAB("android.app.ActionBar$Tab"),
|
||||
DRAWER_LAYOUT("android.support.v4.widget.DrawerLayout"),
|
||||
SLIDING_DRAWER("android.widget.SlidingDrawer"),
|
||||
ICON_MENU("com.android.internal.view.menu.IconMenuView"),
|
||||
TOAST("android.widget.Toast$TN"),
|
||||
DATE_PICKER_DIALOG("android.app.DatePickerDialog"),
|
||||
TIME_PICKER_DIALOG("android.app.TimePickerDialog"),
|
||||
DATE_PICKER("android.widget.DatePicker"),
|
||||
TIME_PICKER("android.widget.TimePicker"),
|
||||
NUMBER_PICKER("android.widget.NumberPicker"),
|
||||
SCROLL_VIEW("android.widget.ScrollView"),
|
||||
HORIZONTAL_SCROLL_VIEW("android.widget.HorizontalScrollView"),
|
||||
KEYBOARD_KEY("android.inputmethodservice.Keyboard$Key");
|
||||
|
||||
@Nullable private final String mValue;
|
||||
|
||||
AccessibilityRole(String type) {
|
||||
mValue = type;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getValue() {
|
||||
return mValue;
|
||||
}
|
||||
|
||||
public static AccessibilityRole fromValue(String value) {
|
||||
for (AccessibilityRole role : AccessibilityRole.values()) {
|
||||
if (role.getValue() != null && role.getValue().equals(value)) {
|
||||
return role;
|
||||
}
|
||||
}
|
||||
return AccessibilityRole.NONE;
|
||||
}
|
||||
}
|
||||
|
||||
private AccessibilityRoleUtil() {
|
||||
// No instances
|
||||
}
|
||||
|
||||
public static AccessibilityRole getRole(View view) {
|
||||
AccessibilityNodeInfoCompat nodeInfo = AccessibilityNodeInfoCompat.obtain();
|
||||
ViewCompat.onInitializeAccessibilityNodeInfo(view, nodeInfo);
|
||||
AccessibilityRole role = getRole(nodeInfo);
|
||||
nodeInfo.recycle();
|
||||
return role;
|
||||
}
|
||||
|
||||
public static AccessibilityRole getRole(AccessibilityNodeInfo nodeInfo) {
|
||||
return getRole(new AccessibilityNodeInfoCompat(nodeInfo));
|
||||
}
|
||||
|
||||
public static AccessibilityRole getRole(AccessibilityNodeInfoCompat nodeInfo) {
|
||||
AccessibilityRole role = AccessibilityRole.fromValue((String) nodeInfo.getClassName());
|
||||
if (role.equals(AccessibilityRole.IMAGE_BUTTON) || role.equals(AccessibilityRole.IMAGE)) {
|
||||
return nodeInfo.isClickable() ? AccessibilityRole.IMAGE_BUTTON : AccessibilityRole.IMAGE;
|
||||
}
|
||||
|
||||
if (role.equals(AccessibilityRole.NONE)) {
|
||||
AccessibilityNodeInfoCompat.CollectionInfoCompat collection = nodeInfo.getCollectionInfo();
|
||||
if (collection != null) {
|
||||
// RecyclerView will be classified as a list or grid.
|
||||
if (collection.getRowCount() > 1 && collection.getColumnCount() > 1) {
|
||||
return AccessibilityRole.GRID;
|
||||
} else {
|
||||
return AccessibilityRole.LIST;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return role;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,357 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.facebook.sonar.plugins.inspector.descriptors.utils;
|
||||
|
||||
import static android.content.Context.ACCESSIBILITY_SERVICE;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Build;
|
||||
import android.support.v4.view.ViewCompat;
|
||||
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewParent;
|
||||
import android.view.accessibility.AccessibilityManager;
|
||||
import android.widget.EditText;
|
||||
import com.facebook.sonar.core.SonarArray;
|
||||
import com.facebook.sonar.core.SonarObject;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* This class provides utility methods for determining certain accessibility properties of {@link
|
||||
* View}s and {@link AccessibilityNodeInfoCompat}s. It is porting some of the checks from {@link
|
||||
* com.googlecode.eyesfree.utils.AccessibilityNodeInfoUtils}, but has stripped many features which
|
||||
* are unnecessary here.
|
||||
*/
|
||||
public final class AccessibilityUtil {
|
||||
private AccessibilityUtil() {}
|
||||
|
||||
public static final EnumMapping sAccessibilityActionMapping =
|
||||
new EnumMapping("UNKNOWN") {
|
||||
{
|
||||
put("FOCUS", AccessibilityNodeInfoCompat.ACTION_FOCUS);
|
||||
put("CLEAR_FOCUS", AccessibilityNodeInfoCompat.ACTION_CLEAR_FOCUS);
|
||||
put("SELECT", AccessibilityNodeInfoCompat.ACTION_SELECT);
|
||||
put("CLEAR_SELECTION", AccessibilityNodeInfoCompat.ACTION_CLEAR_SELECTION);
|
||||
put("CLICK", AccessibilityNodeInfoCompat.ACTION_CLICK);
|
||||
put("LONG_CLICK", AccessibilityNodeInfoCompat.ACTION_LONG_CLICK);
|
||||
put("ACCESSIBILITY_FOCUS", AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS);
|
||||
put(
|
||||
"CLEAR_ACCESSIBILITY_FOCUS",
|
||||
AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
|
||||
put(
|
||||
"NEXT_AT_MOVEMENT_GRANULARITY",
|
||||
AccessibilityNodeInfoCompat.ACTION_NEXT_AT_MOVEMENT_GRANULARITY);
|
||||
put(
|
||||
"PREVIOUS_AT_MOVEMENT_GRANULARITY",
|
||||
AccessibilityNodeInfoCompat.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY);
|
||||
put("NEXT_HTML_ELEMENT", AccessibilityNodeInfoCompat.ACTION_NEXT_HTML_ELEMENT);
|
||||
put("PREVIOUS_HTML_ELEMENT", AccessibilityNodeInfoCompat.ACTION_PREVIOUS_HTML_ELEMENT);
|
||||
put("SCROLL_FORWARD", AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD);
|
||||
put("SCROLL_BACKWARD", AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD);
|
||||
put("CUT", AccessibilityNodeInfoCompat.ACTION_CUT);
|
||||
put("COPY", AccessibilityNodeInfoCompat.ACTION_COPY);
|
||||
put("PASTE", AccessibilityNodeInfoCompat.ACTION_PASTE);
|
||||
put("SET_SELECTION", AccessibilityNodeInfoCompat.ACTION_SET_SELECTION);
|
||||
put("SET_SELECTION", AccessibilityNodeInfoCompat.ACTION_SET_SELECTION);
|
||||
put("EXPAND", AccessibilityNodeInfoCompat.ACTION_EXPAND);
|
||||
put("COLLAPSE", AccessibilityNodeInfoCompat.ACTION_COLLAPSE);
|
||||
put("DISMISS", AccessibilityNodeInfoCompat.ACTION_DISMISS);
|
||||
put("SET_TEXT", AccessibilityNodeInfoCompat.ACTION_SET_TEXT);
|
||||
}
|
||||
};
|
||||
|
||||
public static final EnumMapping sImportantForAccessibilityMapping =
|
||||
new EnumMapping("AUTO") {
|
||||
{
|
||||
put("AUTO", View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
|
||||
put("NO", View.IMPORTANT_FOR_ACCESSIBILITY_NO);
|
||||
put("YES", View.IMPORTANT_FOR_ACCESSIBILITY_YES);
|
||||
put("NO_HIDE_DESCENDANTS", View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a {@link Context}, determine if any accessibility service is running.
|
||||
*
|
||||
* @param context The {@link Context} used to get the {@link AccessibilityManager}.
|
||||
* @return {@code true} if an accessibility service is currently running.
|
||||
*/
|
||||
public static boolean isAccessibilityEnabled(Context context) {
|
||||
return ((AccessibilityManager) context.getSystemService(ACCESSIBILITY_SERVICE)).isEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a sentence describing why a given {@link View} will be ignored by Google's TalkBack
|
||||
* screen reader.
|
||||
*
|
||||
* @param view The {@link View} to evaluate.
|
||||
* @return {@code String} describing why a {@link View} is ignored.
|
||||
*/
|
||||
public static String getTalkbackIgnoredReasons(View view) {
|
||||
final int important = ViewCompat.getImportantForAccessibility(view);
|
||||
|
||||
if (important == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO) {
|
||||
return "View has importantForAccessibility set to 'NO'.";
|
||||
}
|
||||
|
||||
if (important == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS) {
|
||||
return "View has importantForAccessibility set to 'NO_HIDE_DESCENDANTS'.";
|
||||
}
|
||||
|
||||
ViewParent parent = view.getParent();
|
||||
while (parent instanceof View) {
|
||||
if (ViewCompat.getImportantForAccessibility((View) parent)
|
||||
== ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS) {
|
||||
return "An ancestor View has importantForAccessibility set to 'NO_HIDE_DESCENDANTS'.";
|
||||
}
|
||||
parent = parent.getParent();
|
||||
}
|
||||
|
||||
final AccessibilityNodeInfoCompat node = ViewAccessibilityHelper.createNodeInfoFromView(view);
|
||||
if (node == null) {
|
||||
return "AccessibilityNodeInfo cannot be found.";
|
||||
}
|
||||
|
||||
try {
|
||||
if (AccessibilityEvaluationUtil.hasEqualBoundsToViewRoot(node, view)) {
|
||||
return "View has the same dimensions as the View Root.";
|
||||
}
|
||||
|
||||
if (!node.isVisibleToUser()) {
|
||||
return "View is not visible.";
|
||||
}
|
||||
|
||||
if (AccessibilityEvaluationUtil.isAccessibilityFocusable(node, view)) {
|
||||
return "View is actionable, but has no description.";
|
||||
}
|
||||
|
||||
if (AccessibilityEvaluationUtil.hasText(node)) {
|
||||
return "View is not actionable, and an ancestor View has co-opted its description.";
|
||||
}
|
||||
|
||||
return "View is not actionable and has no description.";
|
||||
} finally {
|
||||
node.recycle();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a sentence describing why a given {@link View} will be focusable by Google's TalkBack
|
||||
* screen reader.
|
||||
*
|
||||
* @param view The {@link View} to evaluate.
|
||||
* @return {@code String} describing why a {@link View} is focusable.
|
||||
*/
|
||||
@Nullable
|
||||
public static String getTalkbackFocusableReasons(View view) {
|
||||
final AccessibilityNodeInfoCompat node = ViewAccessibilityHelper.createNodeInfoFromView(view);
|
||||
if (node == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
final boolean hasText = AccessibilityEvaluationUtil.hasText(node);
|
||||
final boolean isCheckable = node.isCheckable();
|
||||
final boolean hasNonActionableSpeakingDescendants =
|
||||
AccessibilityEvaluationUtil.hasNonActionableSpeakingDescendants(node, view);
|
||||
|
||||
if (AccessibilityEvaluationUtil.isActionableForAccessibility(node)) {
|
||||
if (node.getChildCount() <= 0) {
|
||||
return "View is actionable and has no children.";
|
||||
} else if (hasText) {
|
||||
return "View is actionable and has a description.";
|
||||
} else if (isCheckable) {
|
||||
return "View is actionable and checkable.";
|
||||
} else if (hasNonActionableSpeakingDescendants) {
|
||||
return "View is actionable and has non-actionable descendants with descriptions.";
|
||||
}
|
||||
}
|
||||
|
||||
if (AccessibilityEvaluationUtil.isTopLevelScrollItem(node, view)) {
|
||||
if (hasText) {
|
||||
return "View is a direct child of a scrollable container and has a description.";
|
||||
} else if (isCheckable) {
|
||||
return "View is a direct child of a scrollable container and is checkable.";
|
||||
} else if (hasNonActionableSpeakingDescendants) {
|
||||
return "View is a direct child of a scrollable container and has non-actionable "
|
||||
+ "descendants with descriptions.";
|
||||
}
|
||||
}
|
||||
|
||||
if (hasText) {
|
||||
return "View has a description and is not actionable, but has no actionable ancestor.";
|
||||
}
|
||||
|
||||
return null;
|
||||
} finally {
|
||||
node.recycle();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the text that Gogole's TalkBack screen reader will read aloud for a given {@link View}.
|
||||
* This may be any combination of the {@link View}'s {@code text}, {@code contentDescription}, and
|
||||
* the {@code text} and {@code contentDescription} of any ancestor {@link View}.
|
||||
*
|
||||
* <p>Note: This string does not include any additional semantic information that Talkback will
|
||||
* read, such as "Button", or "disabled".
|
||||
*
|
||||
* @param view The {@link View} to evaluate.
|
||||
* @return {@code String} describing why a {@link View} is focusable.
|
||||
*/
|
||||
@Nullable
|
||||
public static CharSequence getTalkbackDescription(View view) {
|
||||
final AccessibilityNodeInfoCompat node = ViewAccessibilityHelper.createNodeInfoFromView(view);
|
||||
if (node == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
final CharSequence contentDescription = node.getContentDescription();
|
||||
final CharSequence nodeText = node.getText();
|
||||
|
||||
final boolean hasNodeText = !TextUtils.isEmpty(nodeText);
|
||||
final boolean isEditText = view instanceof EditText;
|
||||
|
||||
// EditText's prioritize their own text content over a contentDescription
|
||||
if (!TextUtils.isEmpty(contentDescription) && (!isEditText || !hasNodeText)) {
|
||||
return contentDescription;
|
||||
}
|
||||
|
||||
if (hasNodeText) {
|
||||
return nodeText;
|
||||
}
|
||||
|
||||
// If there are child views and no contentDescription the text of all non-focusable children,
|
||||
// comma separated, becomes the description.
|
||||
if (view instanceof ViewGroup) {
|
||||
final StringBuilder concatChildDescription = new StringBuilder();
|
||||
final String separator = ", ";
|
||||
final ViewGroup viewGroup = (ViewGroup) view;
|
||||
|
||||
for (int i = 0, count = viewGroup.getChildCount(); i < count; i++) {
|
||||
final View child = viewGroup.getChildAt(i);
|
||||
|
||||
final AccessibilityNodeInfoCompat childNodeInfo = AccessibilityNodeInfoCompat.obtain();
|
||||
ViewCompat.onInitializeAccessibilityNodeInfo(child, childNodeInfo);
|
||||
|
||||
CharSequence childNodeDescription = null;
|
||||
if (AccessibilityEvaluationUtil.isSpeakingNode(childNodeInfo, child)
|
||||
&& !AccessibilityEvaluationUtil.isAccessibilityFocusable(childNodeInfo, child)) {
|
||||
childNodeDescription = getTalkbackDescription(child);
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(childNodeDescription)) {
|
||||
if (concatChildDescription.length() > 0) {
|
||||
concatChildDescription.append(separator);
|
||||
}
|
||||
concatChildDescription.append(childNodeDescription);
|
||||
}
|
||||
childNodeInfo.recycle();
|
||||
}
|
||||
|
||||
return concatChildDescription.length() > 0 ? concatChildDescription.toString() : null;
|
||||
}
|
||||
|
||||
return null;
|
||||
} finally {
|
||||
node.recycle();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link SonarObject} of useful properties of AccessibilityNodeInfo, to be shown in the
|
||||
* Sonar Layout Inspector. All properties are immutable since they are all derived from various
|
||||
* {@link View} properties.
|
||||
*
|
||||
* @param view The {@link View} to derive the AccessibilityNodeInfo properties from.
|
||||
* @return {@link SonarObject} containing the properties.
|
||||
*/
|
||||
@Nullable
|
||||
public static SonarObject getAccessibilityNodeInfoProperties(View view) {
|
||||
final AccessibilityNodeInfoCompat nodeInfo =
|
||||
ViewAccessibilityHelper.createNodeInfoFromView(view);
|
||||
if (nodeInfo == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final SonarObject.Builder nodeInfoProps = new SonarObject.Builder();
|
||||
final Rect bounds = new Rect();
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
final SonarArray.Builder actionsArrayBuilder = new SonarArray.Builder();
|
||||
for (AccessibilityNodeInfoCompat.AccessibilityActionCompat action :
|
||||
nodeInfo.getActionList()) {
|
||||
final String actionLabel = (String) action.getLabel();
|
||||
if (actionLabel != null) {
|
||||
actionsArrayBuilder.put(actionLabel);
|
||||
} else {
|
||||
actionsArrayBuilder.put(
|
||||
AccessibilityUtil.sAccessibilityActionMapping.get(action.getId(), false));
|
||||
}
|
||||
}
|
||||
nodeInfoProps.put("actions", actionsArrayBuilder.build());
|
||||
}
|
||||
|
||||
nodeInfoProps
|
||||
.put("clickable", nodeInfo.isClickable())
|
||||
.put("content-description", nodeInfo.getContentDescription())
|
||||
.put("text", nodeInfo.getText())
|
||||
.put("focused", nodeInfo.isAccessibilityFocused())
|
||||
.put("long-clickable", nodeInfo.isLongClickable())
|
||||
.put("focusable", nodeInfo.isFocusable());
|
||||
|
||||
nodeInfo.getBoundsInParent(bounds);
|
||||
nodeInfoProps.put(
|
||||
"parent-bounds",
|
||||
new SonarObject.Builder()
|
||||
.put("width", bounds.width())
|
||||
.put("height", bounds.height())
|
||||
.put("top", bounds.top)
|
||||
.put("left", bounds.left)
|
||||
.put("bottom", bounds.bottom)
|
||||
.put("right", bounds.right));
|
||||
|
||||
nodeInfo.getBoundsInScreen(bounds);
|
||||
nodeInfoProps.put(
|
||||
"screen-bounds",
|
||||
new SonarObject.Builder()
|
||||
.put("width", bounds.width())
|
||||
.put("height", bounds.height())
|
||||
.put("top", bounds.top)
|
||||
.put("left", bounds.left)
|
||||
.put("bottom", bounds.bottom)
|
||||
.put("right", bounds.right));
|
||||
|
||||
nodeInfo.recycle();
|
||||
|
||||
return nodeInfoProps.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies a {@link SonarObject.Builder} to add Talkback-specific Accessibiltiy properties to be
|
||||
* shown in the Sonar Layout Inspector.
|
||||
*
|
||||
* @param props The {@link SonarObject.Builder} to add the properties to.
|
||||
* @param view The {@link View} to derive the properties from.
|
||||
*/
|
||||
public static void addTalkbackProperties(SonarObject.Builder props, View view) {
|
||||
if (!AccessibilityEvaluationUtil.isTalkbackFocusable(view)) {
|
||||
props
|
||||
.put("talkback-ignored", true)
|
||||
.put("talkback-ignored-reasons", getTalkbackIgnoredReasons(view));
|
||||
} else {
|
||||
props
|
||||
.put("talkback-focusable", true)
|
||||
.put("talkback-focusable-reasons", getTalkbackFocusableReasons(view))
|
||||
.put("talkback-description", getTalkbackDescription(view));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.facebook.sonar.plugins.inspector.descriptors.utils;
|
||||
|
||||
import static android.view.WindowManager.LayoutParams;
|
||||
|
||||
import android.os.Build;
|
||||
import android.view.View;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public final class AndroidRootResolver {
|
||||
|
||||
private static final String WINDOW_MANAGER_IMPL_CLAZZ = "android.view.WindowManagerImpl";
|
||||
private static final String WINDOW_MANAGER_GLOBAL_CLAZZ = "android.view.WindowManagerGlobal";
|
||||
private static final String VIEWS_FIELD = "mViews";
|
||||
private static final String WINDOW_PARAMS_FIELD = "mParams";
|
||||
private static final String GET_DEFAULT_IMPL = "getDefault";
|
||||
private static final String GET_GLOBAL_INSTANCE = "getInstance";
|
||||
|
||||
private boolean initialized;
|
||||
private Object windowManagerObj;
|
||||
private Field viewsField;
|
||||
private Field paramsField;
|
||||
|
||||
public static class Root {
|
||||
public final View view;
|
||||
public final LayoutParams param;
|
||||
|
||||
private Root(View view, LayoutParams param) {
|
||||
this.view = view;
|
||||
this.param = param;
|
||||
}
|
||||
}
|
||||
|
||||
public @Nullable List<Root> listActiveRoots() {
|
||||
if (!initialized) {
|
||||
initialize();
|
||||
}
|
||||
|
||||
if (null == windowManagerObj) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (null == viewsField) {
|
||||
return null;
|
||||
}
|
||||
if (null == paramsField) {
|
||||
return null;
|
||||
}
|
||||
|
||||
List<View> views = null;
|
||||
List<LayoutParams> params = null;
|
||||
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT < 19) {
|
||||
views = Arrays.asList((View[]) viewsField.get(windowManagerObj));
|
||||
params = Arrays.asList((LayoutParams[]) paramsField.get(windowManagerObj));
|
||||
} else {
|
||||
views = (List<View>) viewsField.get(windowManagerObj);
|
||||
params = (List<LayoutParams>) paramsField.get(windowManagerObj);
|
||||
}
|
||||
} catch (RuntimeException | IllegalAccessException re) {
|
||||
return null;
|
||||
}
|
||||
|
||||
List<Root> roots = new ArrayList<>();
|
||||
for (int i = 0, stop = views.size(); i < stop; i++) {
|
||||
roots.add(new Root(views.get(i), params.get(i)));
|
||||
}
|
||||
return roots;
|
||||
}
|
||||
|
||||
private void initialize() {
|
||||
initialized = true;
|
||||
String accessClass =
|
||||
Build.VERSION.SDK_INT > 16 ? WINDOW_MANAGER_GLOBAL_CLAZZ : WINDOW_MANAGER_IMPL_CLAZZ;
|
||||
String instanceMethod = Build.VERSION.SDK_INT > 16 ? GET_GLOBAL_INSTANCE : GET_DEFAULT_IMPL;
|
||||
|
||||
try {
|
||||
Class<?> clazz = Class.forName(accessClass);
|
||||
Method getMethod = clazz.getMethod(instanceMethod);
|
||||
windowManagerObj = getMethod.invoke(null);
|
||||
viewsField = clazz.getDeclaredField(VIEWS_FIELD);
|
||||
viewsField.setAccessible(true);
|
||||
paramsField = clazz.getDeclaredField(WINDOW_PARAMS_FIELD);
|
||||
paramsField.setAccessible(true);
|
||||
} catch (InvocationTargetException | IllegalAccessException | RuntimeException | NoSuchMethodException | NoSuchFieldException | ClassNotFoundException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
51
android/plugins/inspector/descriptors/utils/EnumMapping.java
Normal file
51
android/plugins/inspector/descriptors/utils/EnumMapping.java
Normal file
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.facebook.sonar.plugins.inspector.descriptors.utils;
|
||||
|
||||
import static com.facebook.sonar.plugins.inspector.InspectorValue.Type.Enum;
|
||||
|
||||
import android.support.v4.util.SimpleArrayMap;
|
||||
import com.facebook.sonar.plugins.inspector.InspectorValue;
|
||||
|
||||
public class EnumMapping {
|
||||
private final SimpleArrayMap<String, Integer> mMapping = new SimpleArrayMap<>();
|
||||
private final String mDefaultKey;
|
||||
|
||||
public EnumMapping(String defaultKey) {
|
||||
mDefaultKey = defaultKey;
|
||||
}
|
||||
|
||||
public void put(String s, int i) {
|
||||
mMapping.put(s, i);
|
||||
}
|
||||
|
||||
public InspectorValue get(final int i) {
|
||||
return get(i, true);
|
||||
}
|
||||
|
||||
public InspectorValue get(final int i, final boolean mutable) {
|
||||
for (int ii = 0, count = mMapping.size(); ii < count; ii++) {
|
||||
if (mMapping.valueAt(ii) == i) {
|
||||
return mutable
|
||||
? InspectorValue.mutable(Enum, mMapping.keyAt(ii))
|
||||
: InspectorValue.immutable(Enum, mMapping.keyAt(ii));
|
||||
}
|
||||
}
|
||||
return mutable
|
||||
? InspectorValue.mutable(Enum, mDefaultKey)
|
||||
: InspectorValue.immutable(Enum, mDefaultKey);
|
||||
}
|
||||
|
||||
public int get(String s) {
|
||||
if (mMapping.containsKey(s)) {
|
||||
return mMapping.get(s);
|
||||
}
|
||||
return mMapping.get(mDefaultKey);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.facebook.sonar.plugins.inspector.descriptors.utils;
|
||||
|
||||
import android.support.v4.view.ViewCompat;
|
||||
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
|
||||
import android.view.View;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/** Class that helps with accessibility by providing useful methods. */
|
||||
public final class ViewAccessibilityHelper {
|
||||
|
||||
/**
|
||||
* Creates and returns an {@link AccessibilityNodeInfoCompat} from the the provided {@link View}.
|
||||
* Note: This does not handle recycling of the {@link AccessibilityNodeInfoCompat}.
|
||||
*
|
||||
* @param view The {@link View} to create the {@link AccessibilityNodeInfoCompat} from.
|
||||
* @return {@link AccessibilityNodeInfoCompat}
|
||||
*/
|
||||
@Nullable
|
||||
public static AccessibilityNodeInfoCompat createNodeInfoFromView(View view) {
|
||||
if (view == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final AccessibilityNodeInfoCompat nodeInfo = AccessibilityNodeInfoCompat.obtain();
|
||||
|
||||
// For some unknown reason, Android seems to occasionally throw a NPE from
|
||||
// onInitializeAccessibilityNodeInfo.
|
||||
try {
|
||||
ViewCompat.onInitializeAccessibilityNodeInfo(view, nodeInfo);
|
||||
} catch (NullPointerException e) {
|
||||
if (nodeInfo != null) {
|
||||
nodeInfo.recycle();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return nodeInfo;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,696 @@
|
||||
// Copyright 2004-present Facebook. All Rights Reserved.
|
||||
|
||||
package com.facebook.litho.sonar;
|
||||
|
||||
import static com.facebook.litho.annotations.ImportantForAccessibility.IMPORTANT_FOR_ACCESSIBILITY_NO;
|
||||
import static com.facebook.litho.annotations.ImportantForAccessibility.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS;
|
||||
import static com.facebook.sonar.plugins.inspector.InspectorValue.Type.Color;
|
||||
import static com.facebook.sonar.plugins.inspector.InspectorValue.Type.Enum;
|
||||
import static com.facebook.sonar.plugins.inspector.InspectorValue.Type.Number;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.support.v4.util.Pair;
|
||||
import android.view.View;
|
||||
import com.facebook.litho.Component;
|
||||
import com.facebook.litho.ComponentContext;
|
||||
import com.facebook.litho.ComponentLifecycle;
|
||||
import com.facebook.litho.DebugComponent;
|
||||
import com.facebook.litho.DebugLayoutNode;
|
||||
import com.facebook.litho.EventHandler;
|
||||
import com.facebook.litho.LithoView;
|
||||
import com.facebook.litho.annotations.Prop;
|
||||
import com.facebook.litho.annotations.State;
|
||||
import com.facebook.litho.reference.Reference;
|
||||
import com.facebook.sonar.core.SonarDynamic;
|
||||
import com.facebook.sonar.core.SonarObject;
|
||||
import com.facebook.sonar.plugins.inspector.HighlightedOverlay;
|
||||
import com.facebook.sonar.plugins.inspector.InspectorValue;
|
||||
import com.facebook.sonar.plugins.inspector.Named;
|
||||
import com.facebook.sonar.plugins.inspector.NodeDescriptor;
|
||||
import com.facebook.sonar.plugins.inspector.Touch;
|
||||
import com.facebook.sonar.plugins.inspector.descriptors.ObjectDescriptor;
|
||||
import com.facebook.sonar.plugins.inspector.descriptors.utils.AccessibilityUtil;
|
||||
import com.facebook.yoga.YogaAlign;
|
||||
import com.facebook.yoga.YogaDirection;
|
||||
import com.facebook.yoga.YogaEdge;
|
||||
import com.facebook.yoga.YogaFlexDirection;
|
||||
import com.facebook.yoga.YogaJustify;
|
||||
import com.facebook.yoga.YogaPositionType;
|
||||
import com.facebook.yoga.YogaValue;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class DebugComponentDescriptor extends NodeDescriptor<DebugComponent> {
|
||||
|
||||
private Map<String, List<Pair<String[], SonarDynamic>>> mOverrides = new HashMap<>();
|
||||
private DebugComponent.Overrider mOverrider =
|
||||
new DebugComponent.Overrider() {
|
||||
@Override
|
||||
public void applyComponentOverrides(String key, Component component) {
|
||||
final List<Pair<String[], SonarDynamic>> overrides = mOverrides.get(key);
|
||||
if (overrides == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (Pair<String[], SonarDynamic> override : overrides) {
|
||||
if (override.first[0].equals("Props")) {
|
||||
applyReflectiveOverride(component, override.first[1], override.second);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void applyStateOverrides(
|
||||
String key, ComponentLifecycle.StateContainer stateContainer) {
|
||||
final List<Pair<String[], SonarDynamic>> overrides = mOverrides.get(key);
|
||||
if (overrides == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (Pair<String[], SonarDynamic> override : overrides) {
|
||||
if (override.first[0].equals("State")) {
|
||||
applyReflectiveOverride(stateContainer, override.first[1], override.second);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void applyLayoutOverrides(String key, DebugLayoutNode node) {
|
||||
final List<Pair<String[], SonarDynamic>> overrides = mOverrides.get(key);
|
||||
if (overrides == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (Pair<String[], SonarDynamic> override : overrides) {
|
||||
if (override.first[0].equals("Layout")) {
|
||||
try {
|
||||
applyLayoutOverride(
|
||||
node,
|
||||
Arrays.copyOfRange(override.first, 1, override.first.length),
|
||||
override.second);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
} else if (override.first[0].equals("Accessibility")) {
|
||||
applyAccessibilityOverride(node, override.first[1], override.second);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void init(DebugComponent node) {
|
||||
// We rely on the LithoView being invalidated when a component hierarchy changes.
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId(DebugComponent node) {
|
||||
return node.getGlobalKey();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName(DebugComponent node) throws Exception {
|
||||
NodeDescriptor componentDescriptor = descriptorForClass(node.getComponent().getClass());
|
||||
if (componentDescriptor.getClass() != ObjectDescriptor.class) {
|
||||
return componentDescriptor.getName(node.getComponent());
|
||||
}
|
||||
return node.getComponent().getSimpleName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getChildCount(DebugComponent node) {
|
||||
if (node.getMountedView() != null || node.getMountedDrawable() != null) {
|
||||
return 1;
|
||||
} else {
|
||||
return node.getChildComponents().size();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getChildAt(DebugComponent node, int index) {
|
||||
final View mountedView = node.getMountedView();
|
||||
final Drawable mountedDrawable = node.getMountedDrawable();
|
||||
|
||||
if (mountedView != null) {
|
||||
return mountedView;
|
||||
} else if (mountedDrawable != null) {
|
||||
return mountedDrawable;
|
||||
} else {
|
||||
return node.getChildComponents().get(index);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Named<SonarObject>> getData(DebugComponent node) throws Exception {
|
||||
NodeDescriptor componentDescriptor = descriptorForClass(node.getComponent().getClass());
|
||||
if (componentDescriptor.getClass() != ObjectDescriptor.class) {
|
||||
return componentDescriptor.getData(node.getComponent());
|
||||
}
|
||||
|
||||
final List<Named<SonarObject>> data = new ArrayList<>();
|
||||
|
||||
final SonarObject layoutData = getLayoutData(node);
|
||||
if (layoutData != null) {
|
||||
data.add(new Named<>("Layout", layoutData));
|
||||
}
|
||||
|
||||
final SonarObject propData = getPropData(node);
|
||||
if (propData != null) {
|
||||
data.add(new Named<>("Props", propData));
|
||||
}
|
||||
|
||||
final SonarObject stateData = getStateData(node);
|
||||
if (stateData != null) {
|
||||
data.add(new Named<>("State", stateData));
|
||||
}
|
||||
|
||||
final SonarObject accessibilityData = getAccessibilityData(node);
|
||||
if (accessibilityData != null) {
|
||||
data.add(new Named<>("Accessibility", accessibilityData));
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static SonarObject getLayoutData(DebugComponent node) {
|
||||
final DebugLayoutNode layout = node.getLayoutNode();
|
||||
if (layout == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final SonarObject.Builder data = new SonarObject.Builder();
|
||||
data.put("background", fromReference(node.getContext(), layout.getBackground()));
|
||||
data.put("foreground", fromDrawable(layout.getForeground()));
|
||||
|
||||
data.put("direction", InspectorValue.mutable(Enum, layout.getLayoutDirection().toString()));
|
||||
data.put("flex-direction", InspectorValue.mutable(Enum, layout.getFlexDirection().toString()));
|
||||
data.put(
|
||||
"justify-content", InspectorValue.mutable(Enum, layout.getJustifyContent().toString()));
|
||||
data.put("align-items", InspectorValue.mutable(Enum, layout.getAlignItems().toString()));
|
||||
data.put("align-self", InspectorValue.mutable(Enum, layout.getAlignSelf().toString()));
|
||||
data.put("align-content", InspectorValue.mutable(Enum, layout.getAlignContent().toString()));
|
||||
data.put("position-type", InspectorValue.mutable(Enum, layout.getPositionType().toString()));
|
||||
|
||||
data.put("flex-grow", fromFloat(layout.getFlexGrow()));
|
||||
data.put("flex-shrink", fromFloat(layout.getFlexShrink()));
|
||||
data.put("flex-basis", fromYogaValue(layout.getFlexBasis()));
|
||||
|
||||
data.put("width", fromYogaValue(layout.getWidth()));
|
||||
data.put("min-width", fromYogaValue(layout.getMinWidth()));
|
||||
data.put("max-width", fromYogaValue(layout.getMaxWidth()));
|
||||
|
||||
data.put("height", fromYogaValue(layout.getHeight()));
|
||||
data.put("min-height", fromYogaValue(layout.getMinHeight()));
|
||||
data.put("max-height", fromYogaValue(layout.getMaxHeight()));
|
||||
|
||||
data.put("aspect-ratio", fromFloat(layout.getAspectRatio()));
|
||||
|
||||
data.put(
|
||||
"margin",
|
||||
new SonarObject.Builder()
|
||||
.put("left", fromYogaValue(layout.getMargin(YogaEdge.LEFT)))
|
||||
.put("top", fromYogaValue(layout.getMargin(YogaEdge.TOP)))
|
||||
.put("right", fromYogaValue(layout.getMargin(YogaEdge.RIGHT)))
|
||||
.put("bottom", fromYogaValue(layout.getMargin(YogaEdge.BOTTOM)))
|
||||
.put("start", fromYogaValue(layout.getMargin(YogaEdge.START)))
|
||||
.put("end", fromYogaValue(layout.getMargin(YogaEdge.END)))
|
||||
.put("horizontal", fromYogaValue(layout.getMargin(YogaEdge.HORIZONTAL)))
|
||||
.put("vertical", fromYogaValue(layout.getMargin(YogaEdge.VERTICAL)))
|
||||
.put("all", fromYogaValue(layout.getMargin(YogaEdge.ALL))));
|
||||
|
||||
data.put(
|
||||
"padding",
|
||||
new SonarObject.Builder()
|
||||
.put("left", fromYogaValue(layout.getPadding(YogaEdge.LEFT)))
|
||||
.put("top", fromYogaValue(layout.getPadding(YogaEdge.TOP)))
|
||||
.put("right", fromYogaValue(layout.getPadding(YogaEdge.RIGHT)))
|
||||
.put("bottom", fromYogaValue(layout.getPadding(YogaEdge.BOTTOM)))
|
||||
.put("start", fromYogaValue(layout.getPadding(YogaEdge.START)))
|
||||
.put("end", fromYogaValue(layout.getPadding(YogaEdge.END)))
|
||||
.put("horizontal", fromYogaValue(layout.getPadding(YogaEdge.HORIZONTAL)))
|
||||
.put("vertical", fromYogaValue(layout.getPadding(YogaEdge.VERTICAL)))
|
||||
.put("all", fromYogaValue(layout.getPadding(YogaEdge.ALL))));
|
||||
|
||||
data.put(
|
||||
"border",
|
||||
new SonarObject.Builder()
|
||||
.put("left", fromFloat(layout.getBorderWidth(YogaEdge.LEFT)))
|
||||
.put("top", fromFloat(layout.getBorderWidth(YogaEdge.TOP)))
|
||||
.put("right", fromFloat(layout.getBorderWidth(YogaEdge.RIGHT)))
|
||||
.put("bottom", fromFloat(layout.getBorderWidth(YogaEdge.BOTTOM)))
|
||||
.put("start", fromFloat(layout.getBorderWidth(YogaEdge.START)))
|
||||
.put("end", fromFloat(layout.getBorderWidth(YogaEdge.END)))
|
||||
.put("horizontal", fromFloat(layout.getBorderWidth(YogaEdge.HORIZONTAL)))
|
||||
.put("vertical", fromFloat(layout.getBorderWidth(YogaEdge.VERTICAL)))
|
||||
.put("all", fromFloat(layout.getBorderWidth(YogaEdge.ALL))));
|
||||
|
||||
data.put(
|
||||
"position",
|
||||
new SonarObject.Builder()
|
||||
.put("left", fromYogaValue(layout.getPosition(YogaEdge.LEFT)))
|
||||
.put("top", fromYogaValue(layout.getPosition(YogaEdge.TOP)))
|
||||
.put("right", fromYogaValue(layout.getPosition(YogaEdge.RIGHT)))
|
||||
.put("bottom", fromYogaValue(layout.getPosition(YogaEdge.BOTTOM)))
|
||||
.put("start", fromYogaValue(layout.getPosition(YogaEdge.START)))
|
||||
.put("end", fromYogaValue(layout.getPosition(YogaEdge.END)))
|
||||
.put("horizontal", fromYogaValue(layout.getPosition(YogaEdge.HORIZONTAL)))
|
||||
.put("vertical", fromYogaValue(layout.getPosition(YogaEdge.VERTICAL)))
|
||||
.put("all", fromYogaValue(layout.getPosition(YogaEdge.ALL))));
|
||||
|
||||
return data.build();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static SonarObject getPropData(DebugComponent node) {
|
||||
if (node.isInternalComponent()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final Component component = node.getComponent();
|
||||
final SonarObject.Builder props = new SonarObject.Builder();
|
||||
|
||||
boolean hasProps = false;
|
||||
for (Field f : component.getClass().getDeclaredFields()) {
|
||||
try {
|
||||
f.setAccessible(true);
|
||||
|
||||
final Prop annotation = f.getAnnotation(Prop.class);
|
||||
if (annotation != null) {
|
||||
switch (annotation.resType()) {
|
||||
case COLOR:
|
||||
props.put(f.getName(), fromColor((Integer) f.get(component)));
|
||||
break;
|
||||
case DRAWABLE:
|
||||
props.put(f.getName(), fromDrawable((Drawable) f.get(component)));
|
||||
break;
|
||||
default:
|
||||
if (f.get(component) != null
|
||||
&& PropWithDescription.class.isAssignableFrom(f.get(component).getClass())) {
|
||||
final Object description =
|
||||
((PropWithDescription) f.get(component))
|
||||
.getSonarLayoutInspectorPropDescription();
|
||||
// Treat the description as immutable for now, because it's a "translation" of the
|
||||
// actual prop,
|
||||
// mutating them is not going to change the original prop.
|
||||
if (description instanceof Map<?, ?>) {
|
||||
final Map<?, ?> descriptionMap = (Map<?, ?>) description;
|
||||
for (Map.Entry<?, ?> entry : descriptionMap.entrySet()) {
|
||||
props.put(
|
||||
entry.getKey().toString(), InspectorValue.immutable(entry.getValue()));
|
||||
}
|
||||
} else {
|
||||
props.put(f.getName(), InspectorValue.immutable(description));
|
||||
}
|
||||
} else {
|
||||
if (isTypeMutable(f.getType())) {
|
||||
props.put(f.getName(), InspectorValue.mutable(f.get(component)));
|
||||
} else {
|
||||
props.put(f.getName(), InspectorValue.immutable(f.get(component)));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
hasProps = true;
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
return hasProps ? props.build() : null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static SonarObject getStateData(DebugComponent node) {
|
||||
if (node.isInternalComponent()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final ComponentLifecycle.StateContainer stateContainer = node.getStateContainer();
|
||||
if (stateContainer == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final SonarObject.Builder state = new SonarObject.Builder();
|
||||
|
||||
boolean hasState = false;
|
||||
for (Field f : stateContainer.getClass().getDeclaredFields()) {
|
||||
try {
|
||||
f.setAccessible(true);
|
||||
|
||||
final State annotation = f.getAnnotation(State.class);
|
||||
if (annotation != null) {
|
||||
if (isTypeMutable(f.getType())) {
|
||||
state.put(f.getName(), InspectorValue.mutable(f.get(stateContainer)));
|
||||
} else {
|
||||
state.put(f.getName(), InspectorValue.immutable(f.get(stateContainer)));
|
||||
}
|
||||
hasState = true;
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
return hasState ? state.build() : null;
|
||||
}
|
||||
|
||||
private static boolean isTypeMutable(Class<?> type) {
|
||||
if (type == int.class || type == Integer.class) {
|
||||
return true;
|
||||
} else if (type == long.class || type == Long.class) {
|
||||
return true;
|
||||
} else if (type == float.class || type == Float.class) {
|
||||
return true;
|
||||
} else if (type == double.class || type == Double.class) {
|
||||
return true;
|
||||
} else if (type == boolean.class || type == Boolean.class) {
|
||||
return true;
|
||||
} else if (type.isAssignableFrom(String.class)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static SonarObject getAccessibilityData(DebugComponent node) {
|
||||
final DebugLayoutNode layout = node.getLayoutNode();
|
||||
if (layout == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final View hostView = node.getComponentHost();
|
||||
final SonarObject.Builder accessibilityProps = new SonarObject.Builder();
|
||||
|
||||
// This needs to be an empty string to be mutable. See t20470623.
|
||||
final CharSequence contentDescription =
|
||||
layout.getContentDescription() != null ? layout.getContentDescription() : "";
|
||||
accessibilityProps.put("content-description", InspectorValue.mutable(contentDescription));
|
||||
accessibilityProps.put("focusable", InspectorValue.mutable(layout.getFocusable()));
|
||||
accessibilityProps.put(
|
||||
"important-for-accessibility",
|
||||
AccessibilityUtil.sImportantForAccessibilityMapping.get(
|
||||
layout.getImportantForAccessibility()));
|
||||
|
||||
// No host view exists, so this component is inherently not accessible. Add the reason why this
|
||||
// is the case and then return.
|
||||
if (hostView == node.getLithoView() || hostView == null) {
|
||||
final int importantForAccessibility = layout.getImportantForAccessibility();
|
||||
final boolean isAccessibilityEnabled =
|
||||
AccessibilityUtil.isAccessibilityEnabled(node.getContext());
|
||||
String ignoredReason;
|
||||
|
||||
if (!isAccessibilityEnabled) {
|
||||
ignoredReason = "No accessibility service is running.";
|
||||
} else if (importantForAccessibility == IMPORTANT_FOR_ACCESSIBILITY_NO) {
|
||||
ignoredReason = "Component has importantForAccessibility set to NO.";
|
||||
} else if (importantForAccessibility == IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS) {
|
||||
ignoredReason = "Component has importantForAccessibility set to NO_HIDE_DESCENDANTS.";
|
||||
} else {
|
||||
ignoredReason = "Component does not have content, or accessibility handlers.";
|
||||
}
|
||||
|
||||
accessibilityProps.put("talkback-ignored", true);
|
||||
accessibilityProps.put("talkback-ignored-reasons", ignoredReason);
|
||||
|
||||
return accessibilityProps.build();
|
||||
}
|
||||
|
||||
accessibilityProps.put(
|
||||
"node-info", AccessibilityUtil.getAccessibilityNodeInfoProperties(hostView));
|
||||
AccessibilityUtil.addTalkbackProperties(accessibilityProps, hostView);
|
||||
|
||||
return accessibilityProps.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(DebugComponent node, String[] path, SonarDynamic value) {
|
||||
List<Pair<String[], SonarDynamic>> overrides = mOverrides.get(node.getGlobalKey());
|
||||
if (overrides == null) {
|
||||
overrides = new ArrayList<>();
|
||||
mOverrides.put(node.getGlobalKey(), overrides);
|
||||
}
|
||||
overrides.add(new Pair<>(path, value));
|
||||
|
||||
node.setOverrider(mOverrider);
|
||||
node.rerender();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Named<String>> getAttributes(DebugComponent node) {
|
||||
final List<Named<String>> attributes = new ArrayList<>();
|
||||
final String key = node.getKey();
|
||||
final String testKey = node.getTestKey();
|
||||
|
||||
if (key != null && key.trim().length() > 0) {
|
||||
attributes.add(new Named<>("key", key));
|
||||
}
|
||||
|
||||
if (testKey != null && testKey.trim().length() > 0) {
|
||||
attributes.add(new Named<>("testKey", testKey));
|
||||
}
|
||||
|
||||
return attributes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setHighlighted(DebugComponent node, boolean selected) {
|
||||
final LithoView lithoView = node.getLithoView();
|
||||
if (lithoView == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selected) {
|
||||
HighlightedOverlay.removeHighlight(lithoView);
|
||||
return;
|
||||
}
|
||||
|
||||
final DebugLayoutNode layout = node.getLayoutNode();
|
||||
final boolean hasNode = layout != null;
|
||||
final Rect margin;
|
||||
if (!node.isRoot()) {
|
||||
margin =
|
||||
new Rect(
|
||||
hasNode ? (int) layout.getResultMargin(YogaEdge.START) : 0,
|
||||
hasNode ? (int) layout.getResultMargin(YogaEdge.TOP) : 0,
|
||||
hasNode ? (int) layout.getResultMargin(YogaEdge.END) : 0,
|
||||
hasNode ? (int) layout.getResultMargin(YogaEdge.BOTTOM) : 0);
|
||||
} else {
|
||||
// Margin not applied if you're at the root
|
||||
margin = new Rect();
|
||||
}
|
||||
|
||||
final Rect padding =
|
||||
new Rect(
|
||||
hasNode ? (int) layout.getResultPadding(YogaEdge.START) : 0,
|
||||
hasNode ? (int) layout.getResultPadding(YogaEdge.TOP) : 0,
|
||||
hasNode ? (int) layout.getResultPadding(YogaEdge.END) : 0,
|
||||
hasNode ? (int) layout.getResultPadding(YogaEdge.BOTTOM) : 0);
|
||||
|
||||
final Rect contentBounds = node.getBoundsInLithoView();
|
||||
HighlightedOverlay.setHighlighted(lithoView, margin, padding, contentBounds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hitTest(DebugComponent node, Touch touch) {
|
||||
for (int i = getChildCount(node) - 1; i >= 0; i--) {
|
||||
final Object child = getChildAt(node, i);
|
||||
if (child instanceof DebugComponent) {
|
||||
final DebugComponent componentChild = (DebugComponent) child;
|
||||
final Rect bounds = componentChild.getBounds();
|
||||
|
||||
if (touch.containedIn(bounds.left, bounds.top, bounds.right, bounds.bottom)) {
|
||||
touch.continueWithOffset(i, bounds.left, bounds.top);
|
||||
return;
|
||||
}
|
||||
} else if (child instanceof View || child instanceof Drawable) {
|
||||
// Components can only mount one view or drawable and its bounds are the same as the
|
||||
// hosting component.
|
||||
touch.continueWithOffset(i, 0, 0);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
touch.finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDecoration(DebugComponent node) throws Exception {
|
||||
if (node.getComponent() != null) {
|
||||
NodeDescriptor componentDescriptor = descriptorForClass(node.getComponent().getClass());
|
||||
if (componentDescriptor.getClass() != ObjectDescriptor.class) {
|
||||
return componentDescriptor.getDecoration(node.getComponent());
|
||||
}
|
||||
}
|
||||
return "litho";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(String query, DebugComponent node) throws Exception {
|
||||
NodeDescriptor descriptor = descriptorForClass(Object.class);
|
||||
return descriptor.matches(query, node);
|
||||
}
|
||||
|
||||
private static void applyAccessibilityOverride(
|
||||
DebugLayoutNode node, String key, SonarDynamic value) {
|
||||
switch (key) {
|
||||
case "focusable":
|
||||
node.setFocusable(value.asBoolean());
|
||||
break;
|
||||
case "important-for-accessibility":
|
||||
node.setImportantForAccessibility(
|
||||
AccessibilityUtil.sImportantForAccessibilityMapping.get(value.asString()));
|
||||
break;
|
||||
case "content-description":
|
||||
node.setContentDescription(value.asString());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static void applyLayoutOverride(DebugLayoutNode node, String[] path, SonarDynamic value) {
|
||||
switch (path[0]) {
|
||||
case "background":
|
||||
node.setBackgroundColor(value.asInt());
|
||||
break;
|
||||
case "foreground":
|
||||
node.setForegroundColor(value.asInt());
|
||||
break;
|
||||
case "direction":
|
||||
node.setLayoutDirection(YogaDirection.valueOf(value.asString().toUpperCase()));
|
||||
break;
|
||||
case "flex-direction":
|
||||
node.setFlexDirection(YogaFlexDirection.valueOf(value.asString().toUpperCase()));
|
||||
break;
|
||||
case "justify-content":
|
||||
node.setJustifyContent(YogaJustify.valueOf(value.asString().toUpperCase()));
|
||||
break;
|
||||
case "align-items":
|
||||
node.setAlignItems(YogaAlign.valueOf(value.asString().toUpperCase()));
|
||||
break;
|
||||
case "align-self":
|
||||
node.setAlignSelf(YogaAlign.valueOf(value.asString().toUpperCase()));
|
||||
break;
|
||||
case "align-content":
|
||||
node.setAlignContent(YogaAlign.valueOf(value.asString().toUpperCase()));
|
||||
break;
|
||||
case "position-type":
|
||||
node.setPositionType(YogaPositionType.valueOf(value.asString().toUpperCase()));
|
||||
break;
|
||||
case "flex-grow":
|
||||
node.setFlexGrow(value.asFloat());
|
||||
break;
|
||||
case "flex-shrink":
|
||||
node.setFlexShrink(value.asFloat());
|
||||
break;
|
||||
case "flex-basis":
|
||||
node.setFlexBasis(YogaValue.parse(value.asString()));
|
||||
break;
|
||||
case "width":
|
||||
node.setWidth(YogaValue.parse(value.asString()));
|
||||
break;
|
||||
case "min-width":
|
||||
node.setMinWidth(YogaValue.parse(value.asString()));
|
||||
break;
|
||||
case "max-width":
|
||||
node.setMaxWidth(YogaValue.parse(value.asString()));
|
||||
break;
|
||||
case "height":
|
||||
node.setHeight(YogaValue.parse(value.asString()));
|
||||
break;
|
||||
case "min-height":
|
||||
node.setMinHeight(YogaValue.parse(value.asString()));
|
||||
break;
|
||||
case "max-height":
|
||||
node.setMaxHeight(YogaValue.parse(value.asString()));
|
||||
break;
|
||||
case "aspect-ratio":
|
||||
node.setAspectRatio(value.asFloat());
|
||||
break;
|
||||
case "margin":
|
||||
node.setMargin(edgeFromString(path[1]), YogaValue.parse(value.asString()));
|
||||
break;
|
||||
case "padding":
|
||||
node.setPadding(edgeFromString(path[1]), YogaValue.parse(value.asString()));
|
||||
break;
|
||||
case "border":
|
||||
node.setBorderWidth(edgeFromString(path[1]), value.asFloat());
|
||||
break;
|
||||
case "position":
|
||||
node.setPosition(edgeFromString(path[1]), YogaValue.parse(value.asString()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static YogaEdge edgeFromString(String s) {
|
||||
return YogaEdge.valueOf(s.toUpperCase());
|
||||
}
|
||||
|
||||
private static void applyReflectiveOverride(Object o, String key, SonarDynamic dynamic) {
|
||||
try {
|
||||
final Field field = o.getClass().getDeclaredField(key);
|
||||
field.setAccessible(true);
|
||||
|
||||
final Class type = field.getType();
|
||||
|
||||
Object value = null;
|
||||
if (type == int.class || type == Integer.class) {
|
||||
value = dynamic.asInt();
|
||||
} else if (type == long.class || type == Long.class) {
|
||||
value = dynamic.asLong();
|
||||
} else if (type == float.class || type == Float.class) {
|
||||
value = dynamic.asFloat();
|
||||
} else if (type == double.class || type == Double.class) {
|
||||
value = dynamic.asDouble();
|
||||
} else if (type == boolean.class || type == Boolean.class) {
|
||||
value = dynamic.asBoolean();
|
||||
} else if (type.isAssignableFrom(String.class)) {
|
||||
value = dynamic.asString();
|
||||
}
|
||||
|
||||
if (value != null) {
|
||||
field.set(o, value);
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
private static InspectorValue fromDrawable(Drawable d) {
|
||||
if (d instanceof ColorDrawable) {
|
||||
return InspectorValue.mutable(Color, ((ColorDrawable) d).getColor());
|
||||
}
|
||||
return InspectorValue.mutable(Color, 0);
|
||||
}
|
||||
|
||||
private static <T extends Drawable> InspectorValue fromReference(
|
||||
ComponentContext c, Reference<T> r) {
|
||||
if (r == null) {
|
||||
return fromDrawable(null);
|
||||
}
|
||||
|
||||
final T d = Reference.acquire(c, r);
|
||||
final InspectorValue v = fromDrawable(d);
|
||||
Reference.release(c, d, r);
|
||||
return v;
|
||||
}
|
||||
|
||||
private static InspectorValue fromFloat(float f) {
|
||||
if (Float.isNaN(f)) {
|
||||
return InspectorValue.mutable(Enum, "undefined");
|
||||
}
|
||||
return InspectorValue.mutable(Number, f);
|
||||
}
|
||||
|
||||
private static InspectorValue fromYogaValue(YogaValue v) {
|
||||
// TODO add support for Type.Dimension or similar
|
||||
return InspectorValue.mutable(Enum, v.toString());
|
||||
}
|
||||
|
||||
private static InspectorValue fromColor(int color) {
|
||||
return InspectorValue.mutable(Color, color);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// Copyright 2004-present Facebook. All Rights Reserved.
|
||||
|
||||
package com.facebook.litho.sonar;
|
||||
|
||||
import com.facebook.litho.DebugComponent;
|
||||
import com.facebook.litho.LithoView;
|
||||
import com.facebook.sonar.plugins.inspector.DescriptorMapping;
|
||||
|
||||
public final class LithoSonarDescriptors {
|
||||
|
||||
public static void add(DescriptorMapping descriptorMapping) {
|
||||
descriptorMapping.register(LithoView.class, new LithoViewDescriptor());
|
||||
descriptorMapping.register(DebugComponent.class, new DebugComponentDescriptor());
|
||||
}
|
||||
}
|
||||
111
android/plugins/inspector/litho-sonar/LithoViewDescriptor.java
Normal file
111
android/plugins/inspector/litho-sonar/LithoViewDescriptor.java
Normal file
@@ -0,0 +1,111 @@
|
||||
// Copyright 2004-present Facebook. All Rights Reserved.
|
||||
|
||||
package com.facebook.litho.sonar;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.view.ViewGroup;
|
||||
import com.facebook.litho.DebugComponent;
|
||||
import com.facebook.litho.LithoView;
|
||||
import com.facebook.sonar.core.SonarDynamic;
|
||||
import com.facebook.sonar.core.SonarObject;
|
||||
import com.facebook.sonar.plugins.inspector.Named;
|
||||
import com.facebook.sonar.plugins.inspector.NodeDescriptor;
|
||||
import com.facebook.sonar.plugins.inspector.Touch;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class LithoViewDescriptor extends NodeDescriptor<LithoView> {
|
||||
|
||||
@Override
|
||||
public void init(LithoView node) throws Exception {
|
||||
node.setOnDirtyMountListener(
|
||||
new LithoView.OnDirtyMountListener() {
|
||||
@Override
|
||||
public void onDirtyMount(LithoView view) {
|
||||
invalidate(view);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId(LithoView node) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(ViewGroup.class);
|
||||
return descriptor.getId(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName(LithoView node) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(ViewGroup.class);
|
||||
return descriptor.getName(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getChildCount(LithoView node) {
|
||||
return DebugComponent.getRootInstance(node) == null ? 0 : 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getChildAt(LithoView node, int index) {
|
||||
return DebugComponent.getRootInstance(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Named<SonarObject>> getData(LithoView node) throws Exception {
|
||||
final List<Named<SonarObject>> props = new ArrayList<>();
|
||||
final NodeDescriptor descriptor = descriptorForClass(ViewGroup.class);
|
||||
final Rect mountedBounds = node.getPreviousMountBounds();
|
||||
|
||||
props.add(
|
||||
0,
|
||||
new Named<>(
|
||||
"LithoView",
|
||||
new SonarObject.Builder()
|
||||
.put(
|
||||
"mountbounds",
|
||||
new SonarObject.Builder()
|
||||
.put("left", mountedBounds.left)
|
||||
.put("top", mountedBounds.top)
|
||||
.put("right", mountedBounds.right)
|
||||
.put("bottom", mountedBounds.bottom))
|
||||
.build()));
|
||||
|
||||
props.addAll(descriptor.getData(node));
|
||||
|
||||
return props;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(LithoView node, String[] path, SonarDynamic value) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(ViewGroup.class);
|
||||
descriptor.setValue(node, path, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Named<String>> getAttributes(LithoView node) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(ViewGroup.class);
|
||||
return descriptor.getAttributes(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setHighlighted(LithoView node, boolean selected) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(ViewGroup.class);
|
||||
descriptor.setHighlighted(node, selected);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hitTest(LithoView node, Touch touch) {
|
||||
touch.continueWithOffset(0, 0, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDecoration(LithoView node) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(ViewGroup.class);
|
||||
return descriptor.getDecoration(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(String query, LithoView node) throws Exception {
|
||||
NodeDescriptor descriptor = descriptorForClass(Object.class);
|
||||
return descriptor.matches(query, node);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright 2004-present Facebook. All Rights Reserved.
|
||||
|
||||
package com.facebook.litho.sonar;
|
||||
|
||||
public interface PropWithDescription {
|
||||
|
||||
Object getSonarLayoutInspectorPropDescription();
|
||||
}
|
||||
69
android/plugins/network/NetworkReporter.java
Normal file
69
android/plugins/network/NetworkReporter.java
Normal file
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.facebook.sonar.plugins.network;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public interface NetworkReporter {
|
||||
void reportRequest(RequestInfo requestInfo);
|
||||
|
||||
void reportResponse(ResponseInfo responseInfo);
|
||||
|
||||
public class Header {
|
||||
public final String name;
|
||||
public final String value;
|
||||
|
||||
public Header(final String name, final String value) {
|
||||
this.name = name;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Header{" + name + ": " + value + "}";
|
||||
}
|
||||
}
|
||||
|
||||
public class RequestInfo {
|
||||
public String requestId;
|
||||
public long timeStamp;
|
||||
public List<Header> headers = new ArrayList<>();
|
||||
public String method;
|
||||
public String uri;
|
||||
public byte[] body;
|
||||
|
||||
public Header getFirstHeader(final String name) {
|
||||
for (Header header : headers) {
|
||||
if (name.equalsIgnoreCase(header.name)) {
|
||||
return header;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public class ResponseInfo {
|
||||
public String requestId;
|
||||
public long timeStamp;
|
||||
public int statusCode;
|
||||
public String statusReason;
|
||||
public List<Header> headers = new ArrayList<>();
|
||||
public byte[] body;
|
||||
|
||||
public Header getFirstHeader(final String name) {
|
||||
for (Header header : headers) {
|
||||
if (name.equalsIgnoreCase(header.name)) {
|
||||
return header;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
22
android/plugins/network/NetworkResponseFormatter.java
Normal file
22
android/plugins/network/NetworkResponseFormatter.java
Normal file
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.facebook.sonar.plugins.network;
|
||||
|
||||
import com.facebook.sonar.plugins.network.NetworkReporter.ResponseInfo;
|
||||
|
||||
public interface NetworkResponseFormatter {
|
||||
|
||||
interface OnCompletionListener {
|
||||
void onCompletion(String json);
|
||||
}
|
||||
|
||||
boolean shouldFormat(ResponseInfo response);
|
||||
|
||||
void format(ResponseInfo response, OnCompletionListener onCompletionListener);
|
||||
}
|
||||
122
android/plugins/network/NetworkSonarPlugin.java
Normal file
122
android/plugins/network/NetworkSonarPlugin.java
Normal file
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.facebook.sonar.plugins.network;
|
||||
|
||||
import android.util.Base64;
|
||||
import com.facebook.sonar.core.ErrorReportingRunnable;
|
||||
import com.facebook.sonar.core.SonarArray;
|
||||
import com.facebook.sonar.core.SonarObject;
|
||||
import com.facebook.sonar.plugins.common.BufferingSonarPlugin;
|
||||
import java.util.List;
|
||||
|
||||
public class NetworkSonarPlugin extends BufferingSonarPlugin implements NetworkReporter {
|
||||
public static final String ID = "Network";
|
||||
|
||||
private final List<NetworkResponseFormatter> mFormatters;
|
||||
|
||||
public NetworkSonarPlugin() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
public NetworkSonarPlugin(List<NetworkResponseFormatter> formatters) {
|
||||
this.mFormatters = formatters;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reportRequest(RequestInfo requestInfo) {
|
||||
final SonarObject request =
|
||||
new SonarObject.Builder()
|
||||
.put("id", requestInfo.requestId)
|
||||
.put("timestamp", requestInfo.timeStamp)
|
||||
.put("method", requestInfo.method)
|
||||
.put("url", requestInfo.uri)
|
||||
.put("headers", toSonarObject(requestInfo.headers))
|
||||
.put("data", toBase64(requestInfo.body))
|
||||
.build();
|
||||
|
||||
send("newRequest", request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reportResponse(final ResponseInfo responseInfo) {
|
||||
final Runnable job =
|
||||
new ErrorReportingRunnable(getConnection()) {
|
||||
@Override
|
||||
protected void runOrThrow() throws Exception {
|
||||
if (shouldStripResponseBody(responseInfo)) {
|
||||
responseInfo.body = null;
|
||||
}
|
||||
|
||||
final SonarObject response =
|
||||
new SonarObject.Builder()
|
||||
.put("id", responseInfo.requestId)
|
||||
.put("timestamp", responseInfo.timeStamp)
|
||||
.put("status", responseInfo.statusCode)
|
||||
.put("reason", responseInfo.statusReason)
|
||||
.put("headers", toSonarObject(responseInfo.headers))
|
||||
.put("data", toBase64(responseInfo.body))
|
||||
.build();
|
||||
|
||||
send("newResponse", response);
|
||||
}
|
||||
};
|
||||
|
||||
if (mFormatters != null) {
|
||||
for (NetworkResponseFormatter formatter : mFormatters) {
|
||||
if (formatter.shouldFormat(responseInfo)) {
|
||||
formatter.format(
|
||||
responseInfo,
|
||||
new NetworkResponseFormatter.OnCompletionListener() {
|
||||
@Override
|
||||
public void onCompletion(final String json) {
|
||||
responseInfo.body = json.getBytes();
|
||||
job.run();
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
job.run();
|
||||
}
|
||||
|
||||
private String toBase64(byte[] bytes) {
|
||||
if (bytes == null) {
|
||||
return null;
|
||||
}
|
||||
return new String(Base64.encode(bytes, Base64.DEFAULT));
|
||||
}
|
||||
|
||||
private SonarArray toSonarObject(List<Header> headers) {
|
||||
final SonarArray.Builder list = new SonarArray.Builder();
|
||||
|
||||
for (Header header : headers) {
|
||||
list.put(new SonarObject.Builder().put("key", header.name).put("value", header.value));
|
||||
}
|
||||
|
||||
return list.build();
|
||||
}
|
||||
|
||||
private static boolean shouldStripResponseBody(ResponseInfo responseInfo) {
|
||||
final Header contentType = responseInfo.getFirstHeader("content-type");
|
||||
if (contentType == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return contentType.value.contains("image/")
|
||||
|| contentType.value.contains("video/")
|
||||
|| contentType.value.contains("application/zip");
|
||||
}
|
||||
}
|
||||
106
android/plugins/network/SonarOkhttpInterceptor.java
Normal file
106
android/plugins/network/SonarOkhttpInterceptor.java
Normal file
@@ -0,0 +1,106 @@
|
||||
// Copyright 2004-present Facebook. All Rights Reserved.
|
||||
package com.facebook.sonar.plugins.network;
|
||||
|
||||
import android.util.Log;
|
||||
import com.facebook.sonar.plugins.network.NetworkReporter.RequestInfo;
|
||||
import com.facebook.sonar.plugins.network.NetworkReporter.ResponseInfo;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
import javax.annotation.Nullable;
|
||||
import okhttp3.Headers;
|
||||
import okhttp3.Interceptor;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.ResponseBody;
|
||||
import okio.Buffer;
|
||||
|
||||
public class SonarOkhttpInterceptor implements Interceptor {
|
||||
|
||||
public @Nullable NetworkSonarPlugin plugin;
|
||||
|
||||
public SonarOkhttpInterceptor() {
|
||||
this.plugin = null;
|
||||
}
|
||||
|
||||
public SonarOkhttpInterceptor(NetworkSonarPlugin plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response intercept(Interceptor.Chain chain) throws IOException {
|
||||
Request request = chain.request();
|
||||
int randInt = randInt(1, Integer.MAX_VALUE);
|
||||
plugin.reportRequest(convertRequest(request, randInt));
|
||||
Response response = chain.proceed(request);
|
||||
ResponseBody body = response.body();
|
||||
ResponseInfo responseInfo = convertResponse(response, body, randInt);
|
||||
plugin.reportResponse(responseInfo);
|
||||
// Creating new response as can't used response.body() more than once
|
||||
return response
|
||||
.newBuilder()
|
||||
.body(ResponseBody.create(body.contentType(), responseInfo.body))
|
||||
.build();
|
||||
}
|
||||
|
||||
private static byte[] bodyToByteArray(final Request request) {
|
||||
|
||||
try {
|
||||
final Request copy = request.newBuilder().build();
|
||||
final Buffer buffer = new Buffer();
|
||||
copy.body().writeTo(buffer);
|
||||
return buffer.readByteArray();
|
||||
} catch (final IOException e) {
|
||||
return e.getMessage().getBytes();
|
||||
}
|
||||
}
|
||||
|
||||
private RequestInfo convertRequest(Request request, int identifier) {
|
||||
List<NetworkReporter.Header> headers = convertHeader(request.headers());
|
||||
RequestInfo info = new RequestInfo();
|
||||
info.requestId = String.valueOf(identifier);
|
||||
info.timeStamp = System.currentTimeMillis();
|
||||
info.headers = headers;
|
||||
info.method = request.method();
|
||||
info.uri = request.url().toString();
|
||||
if (request.body() != null) {
|
||||
info.body = bodyToByteArray(request);
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
private ResponseInfo convertResponse(Response response, ResponseBody body, int identifier) {
|
||||
|
||||
List<NetworkReporter.Header> headers = convertHeader(response.headers());
|
||||
ResponseInfo info = new ResponseInfo();
|
||||
info.requestId = String.valueOf(identifier);
|
||||
info.timeStamp = response.receivedResponseAtMillis();
|
||||
info.statusCode = response.code();
|
||||
info.headers = headers;
|
||||
try {
|
||||
info.body = body.bytes();
|
||||
} catch (IOException e) {
|
||||
Log.e("Sonar", e.toString());
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
private List<NetworkReporter.Header> convertHeader(Headers headers) {
|
||||
List<NetworkReporter.Header> list = new ArrayList<>();
|
||||
|
||||
Set<String> keys = headers.names();
|
||||
for (String key : keys) {
|
||||
list.add(new NetworkReporter.Header(key, headers.get(key)));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private int randInt(int min, int max) {
|
||||
Random rand = new Random();
|
||||
int randomNum = rand.nextInt((max - min) + 1) + min;
|
||||
return randomNum;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.facebook.sonar.plugins.sharedpreferences;
|
||||
|
||||
import static android.content.Context.MODE_PRIVATE;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import com.facebook.sonar.core.SonarConnection;
|
||||
import com.facebook.sonar.core.SonarObject;
|
||||
import com.facebook.sonar.core.SonarPlugin;
|
||||
import com.facebook.sonar.core.SonarReceiver;
|
||||
import com.facebook.sonar.core.SonarResponder;
|
||||
import java.util.Map;
|
||||
|
||||
public class SharedPreferencesSonarPlugin implements SonarPlugin {
|
||||
|
||||
private SonarConnection mConnection;
|
||||
private final SharedPreferences mSharedPreferences;
|
||||
|
||||
public SharedPreferencesSonarPlugin(Context context) {
|
||||
mSharedPreferences = context.getSharedPreferences(context.getPackageName(), MODE_PRIVATE);
|
||||
|
||||
mSharedPreferences.registerOnSharedPreferenceChangeListener(
|
||||
new SharedPreferences.OnSharedPreferenceChangeListener() {
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
|
||||
if (mConnection != null) {
|
||||
mConnection.send(
|
||||
"sharedPreferencesChange",
|
||||
new SonarObject.Builder()
|
||||
.put("name", key)
|
||||
.put("deleted", !mSharedPreferences.contains(key))
|
||||
.put("time", System.currentTimeMillis())
|
||||
.put("value", mSharedPreferences.getAll().get(key))
|
||||
.build());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "Preferences";
|
||||
}
|
||||
|
||||
private SonarObject getSharedPreferencesObject() {
|
||||
final SonarObject.Builder builder = new SonarObject.Builder();
|
||||
final Map<String, ?> map = mSharedPreferences.getAll();
|
||||
|
||||
for (Map.Entry<String, ?> entry : map.entrySet()) {
|
||||
final Object val = entry.getValue();
|
||||
builder.put(entry.getKey(), val);
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnect(SonarConnection connection) {
|
||||
mConnection = connection;
|
||||
|
||||
connection.receive(
|
||||
"getSharedPreferences",
|
||||
new SonarReceiver() {
|
||||
@Override
|
||||
public void onReceive(SonarObject params, SonarResponder responder) {
|
||||
responder.success(getSharedPreferencesObject());
|
||||
}
|
||||
});
|
||||
|
||||
connection.receive(
|
||||
"setSharedPreference",
|
||||
new SonarReceiver() {
|
||||
@Override
|
||||
public void onReceive(SonarObject params, SonarResponder responder)
|
||||
throws IllegalArgumentException {
|
||||
|
||||
String preferenceName = params.getString("preferenceName");
|
||||
Object originalValue = mSharedPreferences.getAll().get(preferenceName);
|
||||
SharedPreferences.Editor editor = mSharedPreferences.edit();
|
||||
|
||||
if (originalValue instanceof Boolean) {
|
||||
editor.putBoolean(preferenceName, params.getBoolean("preferenceValue"));
|
||||
} else if (originalValue instanceof Long) {
|
||||
editor.putLong(preferenceName, params.getLong("preferenceValue"));
|
||||
} else if (originalValue instanceof Integer) {
|
||||
editor.putInt(preferenceName, params.getInt("preferenceValue"));
|
||||
} else if (originalValue instanceof Float) {
|
||||
editor.putFloat(preferenceName, params.getFloat("preferenceValue"));
|
||||
} else if (originalValue instanceof String) {
|
||||
editor.putString(preferenceName, params.getString("preferenceValue"));
|
||||
} else {
|
||||
throw new IllegalArgumentException("Type not supported: " + preferenceName);
|
||||
}
|
||||
|
||||
editor.apply();
|
||||
|
||||
responder.success(getSharedPreferencesObject());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisconnect() {
|
||||
mConnection = null;
|
||||
}
|
||||
}
|
||||
29
android/sample/AndroidManifest.xml
Normal file
29
android/sample/AndroidManifest.xml
Normal file
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.facebook.sonar.sample">
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
|
||||
<uses-sdk
|
||||
android:minSdkVersion="15"
|
||||
android:targetSdkVersion="24"
|
||||
/>
|
||||
|
||||
<application
|
||||
android:name=".SonarSampleApplication"
|
||||
android:label="@string/app_name"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:allowBackup="false"
|
||||
android:theme="@style/NoTitleBarWhiteBG">
|
||||
<activity android:name=".MainActivity">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
|
||||
</manifest>
|
||||
63
android/sample/build.gradle
Normal file
63
android/sample/build.gradle
Normal file
@@ -0,0 +1,63 @@
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
|
||||
compileSdkVersion rootProject.compileSdkVersion
|
||||
buildToolsVersion rootProject.buildToolsVersion
|
||||
defaultConfig {
|
||||
minSdkVersion rootProject.minSdkVersion
|
||||
testInstrumentationRunner 'android.support.test.runner.AndroidJUnitRunner'
|
||||
applicationId "com.facebook.sonar.sample"
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
}
|
||||
sourceSets {
|
||||
main {
|
||||
manifest.srcFile './AndroidManifest.xml'
|
||||
java {
|
||||
srcDir 'src'
|
||||
}
|
||||
res {
|
||||
srcDir 'res'
|
||||
}
|
||||
}
|
||||
}
|
||||
packagingOptions {
|
||||
pickFirst 'lib/armeabi-v7a/libfb.so'
|
||||
pickFirst 'lib/x86/libfb.so'
|
||||
pickFirst 'lib/x86_64/libfb.so'
|
||||
pickFirst 'lib/arm64-v8a/libfb.so'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation 'com.android.support:appcompat-v7:26.1.0'
|
||||
implementation 'com.android.support.constraint:constraint-layout:1.1.0'
|
||||
implementation 'com.android.support:design:26.1.0'
|
||||
testImplementation 'junit:junit:4.12'
|
||||
androidTestImplementation 'com.android.support.test:runner:1.0.2'
|
||||
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
|
||||
// ...
|
||||
// Litho
|
||||
implementation 'com.facebook.litho:litho-core:0.15.0'
|
||||
implementation 'com.facebook.litho:litho-widget:0.15.0'
|
||||
compileOnly 'com.facebook.litho:litho-annotations:0.15.0'
|
||||
|
||||
annotationProcessor 'com.facebook.litho:litho-processor:0.15.0'
|
||||
|
||||
// SoLoader
|
||||
implementation 'com.facebook.soloader:soloader:0.4.1'
|
||||
|
||||
// For integration with Fresco
|
||||
implementation 'com.facebook.litho:litho-fresco:0.15.0'
|
||||
|
||||
// For testing
|
||||
testImplementation 'com.facebook.litho:litho-testing:0.15.0'
|
||||
|
||||
implementation 'com.squareup.okhttp3:okhttp:3.10.0'
|
||||
implementation project(':android')
|
||||
//implementation project(':sonar')
|
||||
}
|
||||
BIN
android/sample/debug.keystore
Normal file
BIN
android/sample/debug.keystore
Normal file
Binary file not shown.
3
android/sample/debug.keystore.properties
Normal file
3
android/sample/debug.keystore.properties
Normal file
@@ -0,0 +1,3 @@
|
||||
key.alias=androiddebugkey
|
||||
key.store.password=android
|
||||
key.alias.password=android
|
||||
BIN
android/sample/res/drawable-hdpi/ic_launcher.png
Normal file
BIN
android/sample/res/drawable-hdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.8 KiB |
BIN
android/sample/res/drawable-mdpi/ic_launcher.png
Normal file
BIN
android/sample/res/drawable-mdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
BIN
android/sample/res/drawable-xhdpi/ic_launcher.png
Normal file
BIN
android/sample/res/drawable-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.4 KiB |
BIN
android/sample/res/drawable-xxhdpi/ic_launcher.png
Normal file
BIN
android/sample/res/drawable-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.7 KiB |
BIN
android/sample/res/drawable-xxxhdpi/ic_launcher.png
Normal file
BIN
android/sample/res/drawable-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.8 KiB |
5
android/sample/res/values/strings.xml
Normal file
5
android/sample/res/values/strings.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<resources>
|
||||
<string name="app_name">Sonar</string>
|
||||
</resources>
|
||||
7
android/sample/res/values/styles.xml
Normal file
7
android/sample/res/values/styles.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<resources>
|
||||
<style name="NoTitleBarWhiteBG" parent="Theme.AppCompat.Light">
|
||||
<item name="android:textColor">#000000</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright 2004-present Facebook. All Rights Reserved.
|
||||
|
||||
package com.facebook.sonar.sample;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import com.facebook.litho.ComponentContext;
|
||||
import com.facebook.litho.LithoView;
|
||||
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
final ComponentContext c = new ComponentContext(this);
|
||||
setContentView(
|
||||
LithoView.create(
|
||||
c,
|
||||
RootComponent.create(c).build()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
// Copyright 2004-present Facebook. All Rights Reserved.
|
||||
|
||||
package com.facebook.sonar.sample;
|
||||
|
||||
import com.facebook.litho.Column;
|
||||
import com.facebook.litho.Component;
|
||||
import com.facebook.litho.ComponentContext;
|
||||
import com.facebook.litho.annotations.LayoutSpec;
|
||||
import com.facebook.litho.annotations.OnCreateLayout;
|
||||
import com.facebook.litho.widget.Text;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.FormBody;
|
||||
import okhttp3.FormBody.Builder;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Call;
|
||||
import okhttp3.Callback;
|
||||
import com.facebook.litho.ClickEvent;
|
||||
import android.util.Log;
|
||||
import java.io.IOException;
|
||||
import com.facebook.litho.annotations.OnEvent;
|
||||
|
||||
@LayoutSpec
|
||||
public class RootComponentSpec {
|
||||
|
||||
@OnCreateLayout
|
||||
static Component onCreateLayout(ComponentContext c) {
|
||||
return Column.create(c)
|
||||
.child(
|
||||
Text.create(c)
|
||||
.text("Tap to hit get request")
|
||||
.key("1")
|
||||
.textSizeSp(20)
|
||||
.clickHandler(RootComponent.hitGetRequest(c)))
|
||||
.child(
|
||||
Text.create(c)
|
||||
.text("Tap to hit post request")
|
||||
.key("2")
|
||||
.textSizeSp(20)
|
||||
.clickHandler(RootComponent.hitPostRequest(c)))
|
||||
.child(
|
||||
Text.create(c)
|
||||
.text("I m just a text")
|
||||
.key("3")
|
||||
.textSizeSp(20))
|
||||
.build();
|
||||
}
|
||||
|
||||
@OnEvent(ClickEvent.class)
|
||||
static void hitGetRequest(ComponentContext c) {
|
||||
|
||||
Request request = new Request.Builder()
|
||||
.url("https://api.github.com/repos/facebook/yoga")
|
||||
.get()
|
||||
.build();
|
||||
SonarSampleApplication.okhttpClient.newCall(request).enqueue(new Callback() {
|
||||
@Override
|
||||
public void onFailure(Call call, IOException e) {
|
||||
e.printStackTrace();
|
||||
Log.d("Sonar", e.getMessage());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResponse(Call call, Response response) throws IOException {
|
||||
if (response.isSuccessful()) {
|
||||
Log.d("Sonar", response.body().string());
|
||||
} else {
|
||||
Log.d("Sonar", "not successful");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@OnEvent(ClickEvent.class)
|
||||
static void hitPostRequest(ComponentContext c) {
|
||||
|
||||
RequestBody formBody = new FormBody.Builder()
|
||||
.add("app", "Sonar")
|
||||
.add("remarks", "Its awesome")
|
||||
.build();
|
||||
|
||||
Request request = new Request.Builder()
|
||||
.url("https://demo9512366.mockable.io/SonarPost")
|
||||
.post(formBody)
|
||||
.build();
|
||||
|
||||
SonarSampleApplication.okhttpClient.newCall(request).enqueue(new Callback() {
|
||||
@Override
|
||||
public void onFailure(Call call, IOException e) {
|
||||
e.printStackTrace();
|
||||
Log.d("Sonar", e.getMessage());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResponse(Call call, Response response) throws IOException {
|
||||
if (response.isSuccessful()) {
|
||||
Log.d("Sonar", response.body().string());
|
||||
} else {
|
||||
Log.d("Sonar", "not successful");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// Copyright 2004-present Facebook. All Rights Reserved.
|
||||
|
||||
package com.facebook.sonar.sample;
|
||||
|
||||
import android.app.Application;
|
||||
import android.net.Network;
|
||||
import android.support.annotation.Nullable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import com.facebook.litho.sonar.LithoSonarDescriptors;
|
||||
import com.facebook.soloader.SoLoader;
|
||||
import com.facebook.sonar.android.utils.SonarUtils;
|
||||
import com.facebook.sonar.android.AndroidSonarClient;
|
||||
import com.facebook.sonar.core.SonarClient;
|
||||
import com.facebook.sonar.plugins.inspector.DescriptorMapping;
|
||||
import com.facebook.sonar.plugins.inspector.InspectorSonarPlugin;
|
||||
import com.facebook.sonar.plugins.network.NetworkSonarPlugin;
|
||||
import com.facebook.sonar.plugins.network.SonarOkhttpInterceptor;
|
||||
import com.facebook.sonar.plugins.network.NetworkResponseFormatter;
|
||||
import okhttp3.OkHttpClient;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class SonarSampleApplication extends Application {
|
||||
|
||||
static public OkHttpClient okhttpClient;
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
SoLoader.init(this, false);
|
||||
|
||||
final SonarClient client = AndroidSonarClient.getInstance(this);
|
||||
final DescriptorMapping descriptorMapping = DescriptorMapping.withDefaults();
|
||||
|
||||
NetworkSonarPlugin networkPlugin = new NetworkSonarPlugin();
|
||||
SonarOkhttpInterceptor interceptor = new SonarOkhttpInterceptor(networkPlugin);
|
||||
|
||||
okhttpClient = new OkHttpClient.Builder()
|
||||
.addNetworkInterceptor(interceptor)
|
||||
.connectTimeout(60, TimeUnit.SECONDS)
|
||||
.readTimeout(60, TimeUnit.SECONDS)
|
||||
.writeTimeout(10, TimeUnit.MINUTES)
|
||||
.build();
|
||||
|
||||
LithoSonarDescriptors.add(descriptorMapping);
|
||||
client.addPlugin(new InspectorSonarPlugin(this, descriptorMapping));
|
||||
client.addPlugin(networkPlugin);
|
||||
client.start();
|
||||
}
|
||||
}
|
||||
56
android/testing/SonarConnectionMock.java
Normal file
56
android/testing/SonarConnectionMock.java
Normal file
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
package com.facebook.sonar.testing;
|
||||
|
||||
import com.facebook.sonar.core.SonarArray;
|
||||
import com.facebook.sonar.core.SonarConnection;
|
||||
import com.facebook.sonar.core.SonarObject;
|
||||
import com.facebook.sonar.core.SonarReceiver;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class SonarConnectionMock implements SonarConnection {
|
||||
public final Map<String, SonarReceiver> receivers = new HashMap<>();
|
||||
public final Map<String, List<Object>> sent = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public void send(String method, SonarObject params) {
|
||||
final List<Object> paramList;
|
||||
if (sent.containsKey(method)) {
|
||||
paramList = sent.get(method);
|
||||
} else {
|
||||
paramList = new ArrayList<>();
|
||||
sent.put(method, paramList);
|
||||
}
|
||||
|
||||
paramList.add(params);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void send(String method, SonarArray params) {
|
||||
final List<Object> paramList;
|
||||
if (sent.containsKey(method)) {
|
||||
paramList = sent.get(method);
|
||||
} else {
|
||||
paramList = new ArrayList<>();
|
||||
sent.put(method, paramList);
|
||||
}
|
||||
|
||||
paramList.add(params);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reportError(Throwable throwable) {}
|
||||
|
||||
@Override
|
||||
public void receive(String method, SonarReceiver receiver) {
|
||||
receivers.put(method, receiver);
|
||||
}
|
||||
}
|
||||
39
android/testing/SonarResponderMock.java
Normal file
39
android/testing/SonarResponderMock.java
Normal 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);
|
||||
}
|
||||
}
|
||||
49
android/tests/plugins/console/ConsoleSonarPluginTest.java
Normal file
49
android/tests/plugins/console/ConsoleSonarPluginTest.java
Normal 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);
|
||||
}
|
||||
}
|
||||
109
android/tests/plugins/console/JavascriptSessionTest.java
Normal file
109
android/tests/plugins/console/JavascriptSessionTest.java
Normal 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);
|
||||
}
|
||||
}
|
||||
90
android/tests/plugins/inspector/ApplicationWrapperTest.java
Normal file
90
android/tests/plugins/inspector/ApplicationWrapperTest.java
Normal 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));
|
||||
}
|
||||
}
|
||||
129
android/tests/plugins/inspector/DescriptorMappingTest.java
Normal file
129
android/tests/plugins/inspector/DescriptorMappingTest.java
Normal file
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
package com.facebook.sonar.plugins.inspector;
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
424
android/tests/plugins/inspector/InspectorSonarPluginTest.java
Normal file
424
android/tests/plugins/inspector/InspectorSonarPluginTest.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
4
android/third-party/DoubleConversion/ApplicationManifest.xml
vendored
Normal file
4
android/third-party/DoubleConversion/ApplicationManifest.xml
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.doubleconversion">
|
||||
</manifest>
|
||||
12
android/third-party/DoubleConversion/CMakeLists.txt
vendored
Normal file
12
android/third-party/DoubleConversion/CMakeLists.txt
vendored
Normal 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})
|
||||
34
android/third-party/DoubleConversion/build.gradle
vendored
Normal file
34
android/third-party/DoubleConversion/build.gradle
vendored
Normal 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'
|
||||
}
|
||||
}
|
||||
}
|
||||
4
android/third-party/Folly/ApplicationManifest.xml
vendored
Normal file
4
android/third-party/Folly/ApplicationManifest.xml
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.folly">
|
||||
</manifest>
|
||||
81
android/third-party/Folly/CMakeLists.txt
vendored
Normal file
81
android/third-party/Folly/CMakeLists.txt
vendored
Normal 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
39
android/third-party/Folly/build.gradle
vendored
Normal 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
Reference in New Issue
Block a user