mirror of
https://github.com/klzgrad/naiveproxy.git
synced 2025-02-26 20:03:26 +03:00
330 lines
11 KiB
C++
330 lines
11 KiB
C++
// Copyright 2024 The Chromium Authors
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
#include "base/android/input_hint_checker.h"
|
|
|
|
#include <jni.h>
|
|
#include <pthread.h>
|
|
|
|
#include "base/android/jni_android.h"
|
|
#include "base/android/jni_string.h"
|
|
#include "base/feature_list.h"
|
|
#include "base/metrics/field_trial_params.h"
|
|
#include "base/metrics/histogram_functions.h"
|
|
#include "base/no_destructor.h"
|
|
#include "base/time/time.h"
|
|
|
|
// Must come after all headers that specialize FromJniType() / ToJniType().
|
|
#include "base/base_jni/InputHintChecker_jni.h"
|
|
|
|
namespace base::android {
|
|
|
|
enum class InputHintChecker::InitState {
|
|
kNotStarted,
|
|
kInProgress,
|
|
kInitialized,
|
|
kFailedToInitialize
|
|
};
|
|
|
|
namespace {
|
|
|
|
bool g_input_hint_enabled;
|
|
base::TimeDelta g_poll_interval;
|
|
InputHintChecker* g_test_instance;
|
|
|
|
} // namespace
|
|
|
|
// Whether to fetch the input hint from the system. When disabled, pretends
|
|
// that no input is ever queued.
|
|
BASE_EXPORT
|
|
BASE_FEATURE(kYieldWithInputHint,
|
|
"YieldWithInputHint",
|
|
base::FEATURE_DISABLED_BY_DEFAULT);
|
|
|
|
// Min time delta between checks for the input hint. Must be a smaller than
|
|
// time to produce a frame, but a bit longer than the time it takes to retrieve
|
|
// the hint.
|
|
const base::FeatureParam<int> kPollIntervalMillisParam{&kYieldWithInputHint,
|
|
"poll_interval_ms", 3};
|
|
|
|
// Class calling a private method of InputHintChecker.
|
|
// This allows not to declare the method called by pthread_create in the public
|
|
// header.
|
|
class InputHintChecker::OffThreadInitInvoker {
|
|
public:
|
|
// Called by pthread_create().
|
|
static void* Run(void* opaque) {
|
|
InputHintChecker::GetInstance().RunOffThreadInitialization();
|
|
return nullptr;
|
|
}
|
|
};
|
|
|
|
InputHintChecker::InputHintChecker() : init_state_(InitState::kNotStarted) {}
|
|
|
|
InputHintChecker::~InputHintChecker() = default;
|
|
|
|
// static
|
|
void InputHintChecker::InitializeFeatures() {
|
|
bool is_enabled = base::FeatureList::IsEnabled(kYieldWithInputHint);
|
|
g_input_hint_enabled = is_enabled;
|
|
if (is_enabled) {
|
|
g_poll_interval = Milliseconds(kPollIntervalMillisParam.Get());
|
|
}
|
|
}
|
|
|
|
void InputHintChecker::SetView(JNIEnv* env, jobject root_view) {
|
|
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
|
|
InitState state = FetchState();
|
|
if (state == InitState::kFailedToInitialize) {
|
|
return;
|
|
}
|
|
view_ = JavaObjectWeakGlobalRef(env, root_view);
|
|
if (!root_view) {
|
|
return;
|
|
}
|
|
if (state == InitState::kNotStarted) {
|
|
// Store the View.class and continue initialization on another thread. A
|
|
// separate non-Java thread is required to obtain a reference to
|
|
// j.l.reflect.Method via double-reflection.
|
|
TransitionToState(InitState::kInProgress);
|
|
view_class_ =
|
|
ScopedJavaGlobalRef<jobject>(env, env->GetObjectClass(root_view));
|
|
pthread_t new_thread;
|
|
if (pthread_create(&new_thread, nullptr, OffThreadInitInvoker::Run,
|
|
nullptr) != 0) {
|
|
PLOG(ERROR) << "pthread_create";
|
|
TransitionToState(InitState::kFailedToInitialize);
|
|
}
|
|
}
|
|
}
|
|
|
|
// static
|
|
bool InputHintChecker::HasInput() {
|
|
if (!g_input_hint_enabled) {
|
|
return false;
|
|
}
|
|
return GetInstance().HasInputImplWithThrottling();
|
|
}
|
|
|
|
bool InputHintChecker::IsInitializedForTesting() {
|
|
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
|
|
return FetchState() == InitState::kInitialized;
|
|
}
|
|
|
|
bool InputHintChecker::FailedToInitializeForTesting() {
|
|
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
|
|
return FetchState() == InitState::kFailedToInitialize;
|
|
}
|
|
|
|
bool InputHintChecker::HasInputImplWithThrottling() {
|
|
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
|
|
|
|
// Early return if off-thread initialization has not succeeded yet.
|
|
InitState state = FetchState();
|
|
if (state != InitState::kInitialized) {
|
|
return false;
|
|
}
|
|
|
|
// Input processing is associated with the root view. Early return when the
|
|
// root view is not available. It can happen in cases like multi-window.
|
|
JNIEnv* env = AttachCurrentThread();
|
|
ScopedJavaLocalRef<jobject> scoped_view = view_.get(env);
|
|
if (!scoped_view) {
|
|
return false;
|
|
}
|
|
|
|
// Throttle.
|
|
auto now = base::TimeTicks::Now();
|
|
if (last_checked_.is_null() || (now - last_checked_) >= g_poll_interval) {
|
|
last_checked_ = now;
|
|
} else {
|
|
return false;
|
|
}
|
|
|
|
return HasInputImpl(env, scoped_view.obj());
|
|
}
|
|
|
|
bool InputHintChecker::HasInputImplNoThrottlingForTesting(_JNIEnv* env) {
|
|
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
|
|
if (FetchState() != InitState::kInitialized) {
|
|
return false;
|
|
}
|
|
ScopedJavaLocalRef<jobject> scoped_view = view_.get(env);
|
|
CHECK(scoped_view.obj());
|
|
return HasInputImpl(env, scoped_view.obj());
|
|
}
|
|
|
|
bool InputHintChecker::HasInputImplWithThrottlingForTesting(_JNIEnv* env) {
|
|
if (FetchState() != InitState::kInitialized) {
|
|
return false;
|
|
}
|
|
return HasInputImplWithThrottling();
|
|
}
|
|
|
|
bool InputHintChecker::HasInputImpl(JNIEnv* env, jobject o) {
|
|
auto has_input_result = ScopedJavaLocalRef<jobject>::Adopt(
|
|
env, env->CallObjectMethod(reflect_method_for_has_input_.obj(),
|
|
invoke_id_, o, nullptr));
|
|
if (ClearException(env)) {
|
|
LOG(ERROR) << "Exception when calling reflect_method_for_has_input_";
|
|
TransitionToState(InitState::kFailedToInitialize);
|
|
return false;
|
|
}
|
|
if (!has_input_result) {
|
|
LOG(ERROR) << "Returned null from reflection call";
|
|
TransitionToState(InitState::kFailedToInitialize);
|
|
return false;
|
|
}
|
|
|
|
// Convert result to bool and return.
|
|
bool value = static_cast<bool>(
|
|
env->CallBooleanMethod(has_input_result.obj(), boolean_value_id_));
|
|
if (ClearException(env)) {
|
|
LOG(ERROR) << "Exception when converting to boolean";
|
|
TransitionToState(InitState::kFailedToInitialize);
|
|
return false;
|
|
}
|
|
return value;
|
|
}
|
|
|
|
InputHintChecker::InitState InputHintChecker::FetchState() const {
|
|
return init_state_.load(std::memory_order_acquire);
|
|
}
|
|
|
|
// These values are persisted to logs. Entries should not be renumbered and
|
|
// numeric values should never be reused.
|
|
enum class InitializationResult {
|
|
kSuccess = 0,
|
|
kFailure = 1,
|
|
kMaxValue = kFailure,
|
|
};
|
|
|
|
void InputHintChecker::TransitionToState(InitState new_state) {
|
|
DCHECK_NE(new_state, FetchState());
|
|
if (new_state == InitState::kInitialized ||
|
|
new_state == InitState::kFailedToInitialize) {
|
|
InitializationResult r = (new_state == InitState::kInitialized)
|
|
? InitializationResult::kSuccess
|
|
: InitializationResult::kFailure;
|
|
UmaHistogramEnumeration("Android.InputHintChecker.InitializationResult", r);
|
|
}
|
|
init_state_.store(new_state, std::memory_order_release);
|
|
}
|
|
|
|
void InputHintChecker::RunOffThreadInitialization() {
|
|
JNIEnv* env = AttachCurrentThread();
|
|
InitGlobalRefsAndMethodIds(env);
|
|
DetachFromVM();
|
|
}
|
|
|
|
void InputHintChecker::InitGlobalRefsAndMethodIds(JNIEnv* env) {
|
|
// Obtain j.l.reflect.Method using View.class.getMethod("probablyHasInput",
|
|
// "...").
|
|
jclass view_class = env->GetObjectClass(view_class_.obj());
|
|
if (ClearException(env)) {
|
|
LOG(ERROR) << "exception on GetObjectClass(view)";
|
|
TransitionToState(InitState::kFailedToInitialize);
|
|
return;
|
|
}
|
|
jmethodID get_method_id = env->GetMethodID(
|
|
view_class, "getMethod",
|
|
"(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;");
|
|
if (ClearException(env)) {
|
|
LOG(ERROR) << "exception when looking for method getMethod()";
|
|
TransitionToState(InitState::kFailedToInitialize);
|
|
return;
|
|
}
|
|
ScopedJavaLocalRef<jstring> has_input_string =
|
|
ConvertUTF8ToJavaString(env, "probablyHasInput");
|
|
auto method = ScopedJavaLocalRef<jobject>::Adopt(
|
|
env, env->CallObjectMethod(view_class_.obj(), get_method_id,
|
|
has_input_string.obj(), nullptr));
|
|
if (ClearException(env)) {
|
|
LOG(ERROR) << "exception when calling getMethod(probablyHasInput)";
|
|
TransitionToState(InitState::kFailedToInitialize);
|
|
return;
|
|
}
|
|
if (!method) {
|
|
LOG(ERROR) << "got null from getMethod(probablyHasInput)";
|
|
TransitionToState(InitState::kFailedToInitialize);
|
|
return;
|
|
}
|
|
|
|
// Cache useful members for further calling Method.invoke(view).
|
|
reflect_method_for_has_input_ = ScopedJavaGlobalRef<jobject>(method);
|
|
jclass method_class =
|
|
env->GetObjectClass(reflect_method_for_has_input_.obj());
|
|
if (ClearException(env) || !method_class) {
|
|
LOG(ERROR) << "exception on GetObjectClass(getMethod) or null returned";
|
|
TransitionToState(InitState::kFailedToInitialize);
|
|
return;
|
|
}
|
|
invoke_id_ = env->GetMethodID(
|
|
method_class, "invoke",
|
|
"(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;");
|
|
if (ClearException(env)) {
|
|
LOG(ERROR) << "exception when looking for invoke() of getMethod()";
|
|
TransitionToState(InitState::kFailedToInitialize);
|
|
return;
|
|
}
|
|
jclass boolean_class = env->FindClass("java/lang/Boolean");
|
|
if (ClearException(env) || !boolean_class) {
|
|
LOG(ERROR) << "exception when looking for class Boolean or null returned";
|
|
TransitionToState(InitState::kFailedToInitialize);
|
|
return;
|
|
}
|
|
boolean_value_id_ = env->GetMethodID(boolean_class, "booleanValue", "()Z");
|
|
if (ClearException(env)) {
|
|
LOG(ERROR) << "exception when looking for method booleanValue";
|
|
TransitionToState(InitState::kFailedToInitialize);
|
|
return;
|
|
}
|
|
|
|
// Publish the obtained members to the thread observing kInitialized.
|
|
TransitionToState(InitState::kInitialized);
|
|
}
|
|
|
|
InputHintChecker& InputHintChecker::GetInstance() {
|
|
static NoDestructor<InputHintChecker> checker;
|
|
if (g_test_instance) {
|
|
return *g_test_instance;
|
|
}
|
|
return *checker.get();
|
|
}
|
|
|
|
InputHintChecker::ScopedOverrideInstance::ScopedOverrideInstance(
|
|
InputHintChecker* checker) {
|
|
g_test_instance = checker;
|
|
}
|
|
|
|
InputHintChecker::ScopedOverrideInstance::~ScopedOverrideInstance() {
|
|
g_test_instance = nullptr;
|
|
}
|
|
|
|
void JNI_InputHintChecker_SetView(_JNIEnv* env,
|
|
const JavaParamRef<jobject>& v) {
|
|
InputHintChecker::GetInstance().SetView(env, v.obj());
|
|
}
|
|
|
|
jboolean JNI_InputHintChecker_IsInitializedForTesting(_JNIEnv* env) {
|
|
return InputHintChecker::GetInstance().IsInitializedForTesting(); // IN-TEST
|
|
}
|
|
|
|
jboolean JNI_InputHintChecker_FailedToInitializeForTesting(_JNIEnv* env) {
|
|
return InputHintChecker::GetInstance()
|
|
.FailedToInitializeForTesting(); // IN-TEST
|
|
}
|
|
|
|
jboolean JNI_InputHintChecker_HasInputForTesting(_JNIEnv* env) {
|
|
InputHintChecker& checker = InputHintChecker::GetInstance();
|
|
return checker.HasInputImplNoThrottlingForTesting(env); // IN-TEST
|
|
}
|
|
|
|
jboolean JNI_InputHintChecker_HasInputWithThrottlingForTesting(_JNIEnv* env) {
|
|
InputHintChecker& checker = InputHintChecker::GetInstance();
|
|
return checker.HasInputImplWithThrottlingForTesting(env); // IN-TEST
|
|
}
|
|
|
|
} // namespace base::android
|