1 // Copyright 2024 The Chromium Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "base/android/input_hint_checker.h"
6
7 #include <jni.h>
8 #include <pthread.h>
9
10 #include "base/android/jni_android.h"
11 #include "base/android/jni_string.h"
12 #include "base/feature_list.h"
13 #include "base/metrics/field_trial_params.h"
14 #include "base/metrics/histogram_functions.h"
15 #include "base/metrics/histogram_macros.h"
16 #include "base/no_destructor.h"
17 #include "base/time/time.h"
18
19 // Must come after all headers that specialize FromJniType() / ToJniType().
20 #include "base/base_jni/InputHintChecker_jni.h"
21
22 namespace base::android {
23
24 enum class InputHintChecker::InitState {
25 kNotStarted,
26 kInProgress,
27 kInitialized,
28 kFailedToInitialize
29 };
30
31 namespace {
32
33 bool g_input_hint_enabled;
34 base::TimeDelta g_poll_interval;
35 InputHintChecker* g_test_instance;
36
37 } // namespace
38
39 // Whether to fetch the input hint from the system. When disabled, pretends
40 // that no input is ever queued.
41 BASE_EXPORT
42 BASE_FEATURE(kYieldWithInputHint,
43 "YieldWithInputHint",
44 base::FEATURE_DISABLED_BY_DEFAULT);
45
46 // Min time delta between checks for the input hint. Must be a smaller than
47 // time to produce a frame, but a bit longer than the time it takes to retrieve
48 // the hint.
49 const base::FeatureParam<int> kPollIntervalMillisParam{&kYieldWithInputHint,
50 "poll_interval_ms", 3};
51
52 // Class calling a private method of InputHintChecker.
53 // This allows not to declare the method called by pthread_create in the public
54 // header.
55 class InputHintChecker::OffThreadInitInvoker {
56 public:
57 // Called by pthread_create().
Run(void * opaque)58 static void* Run(void* opaque) {
59 InputHintChecker::GetInstance().RunOffThreadInitialization();
60 return nullptr;
61 }
62 };
63
InputHintChecker()64 InputHintChecker::InputHintChecker() : init_state_(InitState::kNotStarted) {}
65
66 InputHintChecker::~InputHintChecker() = default;
67
68 // static
InitializeFeatures()69 void InputHintChecker::InitializeFeatures() {
70 bool is_enabled = base::FeatureList::IsEnabled(kYieldWithInputHint);
71 g_input_hint_enabled = is_enabled;
72 if (is_enabled) {
73 g_poll_interval = Milliseconds(kPollIntervalMillisParam.Get());
74 }
75 }
76
SetView(JNIEnv * env,const jni_zero::JavaParamRef<jobject> & root_view)77 void InputHintChecker::SetView(
78 JNIEnv* env,
79 const jni_zero::JavaParamRef<jobject>& root_view) {
80 DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
81 InitState state = FetchState();
82 if (state == InitState::kFailedToInitialize) {
83 return;
84 }
85 view_ = JavaObjectWeakGlobalRef(env, root_view);
86 if (!root_view) {
87 return;
88 }
89 if (state == InitState::kNotStarted) {
90 // Store the View.class and continue initialization on another thread. A
91 // separate non-Java thread is required to obtain a reference to
92 // j.l.reflect.Method via double-reflection.
93 TransitionToState(InitState::kInProgress);
94 view_class_ =
95 ScopedJavaGlobalRef<jobject>(env, env->GetObjectClass(root_view.obj()));
96 pthread_t new_thread;
97 if (pthread_create(&new_thread, nullptr, OffThreadInitInvoker::Run,
98 nullptr) != 0) {
99 PLOG(ERROR) << "pthread_create";
100 TransitionToState(InitState::kFailedToInitialize);
101 }
102 }
103 }
104
105 // static
HasInput()106 bool InputHintChecker::HasInput() {
107 if (!g_input_hint_enabled) {
108 return false;
109 }
110 return GetInstance().HasInputImplWithThrottling();
111 }
112
IsInitializedForTesting()113 bool InputHintChecker::IsInitializedForTesting() {
114 DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
115 return FetchState() == InitState::kInitialized;
116 }
117
FailedToInitializeForTesting()118 bool InputHintChecker::FailedToInitializeForTesting() {
119 DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
120 return FetchState() == InitState::kFailedToInitialize;
121 }
122
HasInputImplWithThrottling()123 bool InputHintChecker::HasInputImplWithThrottling() {
124 DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
125
126 // Early return if off-thread initialization has not succeeded yet.
127 InitState state = FetchState();
128 if (state != InitState::kInitialized) {
129 return false;
130 }
131
132 // Input processing is associated with the root view. Early return when the
133 // root view is not available. It can happen in cases like multi-window.
134 JNIEnv* env = AttachCurrentThread();
135 ScopedJavaLocalRef<jobject> scoped_view = view_.get(env);
136 if (!scoped_view) {
137 return false;
138 }
139
140 // Throttle.
141 auto now = base::TimeTicks::Now();
142 if (last_checked_.is_null() || (now - last_checked_) >= g_poll_interval) {
143 last_checked_ = now;
144 } else {
145 return false;
146 }
147
148 return HasInputImpl(env, scoped_view.obj());
149 }
150
HasInputImplNoThrottlingForTesting(_JNIEnv * env)151 bool InputHintChecker::HasInputImplNoThrottlingForTesting(_JNIEnv* env) {
152 DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
153 if (FetchState() != InitState::kInitialized) {
154 return false;
155 }
156 ScopedJavaLocalRef<jobject> scoped_view = view_.get(env);
157 CHECK(scoped_view.obj());
158 return HasInputImpl(env, scoped_view.obj());
159 }
160
HasInputImplWithThrottlingForTesting(_JNIEnv * env)161 bool InputHintChecker::HasInputImplWithThrottlingForTesting(_JNIEnv* env) {
162 if (FetchState() != InitState::kInitialized) {
163 return false;
164 }
165 return HasInputImplWithThrottling();
166 }
167
HasInputImpl(JNIEnv * env,jobject o)168 bool InputHintChecker::HasInputImpl(JNIEnv* env, jobject o) {
169 auto has_input_result = ScopedJavaLocalRef<jobject>::Adopt(
170 env, env->CallObjectMethod(reflect_method_for_has_input_.obj(),
171 invoke_id_, o, nullptr));
172 if (ClearException(env)) {
173 LOG(ERROR) << "Exception when calling reflect_method_for_has_input_";
174 TransitionToState(InitState::kFailedToInitialize);
175 return false;
176 }
177 if (!has_input_result) {
178 LOG(ERROR) << "Returned null from reflection call";
179 TransitionToState(InitState::kFailedToInitialize);
180 return false;
181 }
182
183 // Convert result to bool and return.
184 bool value = static_cast<bool>(
185 env->CallBooleanMethod(has_input_result.obj(), boolean_value_id_));
186 if (ClearException(env)) {
187 LOG(ERROR) << "Exception when converting to boolean";
188 TransitionToState(InitState::kFailedToInitialize);
189 return false;
190 }
191 return value;
192 }
193
FetchState() const194 InputHintChecker::InitState InputHintChecker::FetchState() const {
195 return init_state_.load(std::memory_order_acquire);
196 }
197
198 // These values are persisted to logs. Entries should not be renumbered and
199 // numeric values should never be reused.
200 enum class InitializationResult {
201 kSuccess = 0,
202 kFailure = 1,
203 kMaxValue = kFailure,
204 };
205
TransitionToState(InitState new_state)206 void InputHintChecker::TransitionToState(InitState new_state) {
207 DCHECK_NE(new_state, FetchState());
208 if (new_state == InitState::kInitialized ||
209 new_state == InitState::kFailedToInitialize) {
210 InitializationResult r = (new_state == InitState::kInitialized)
211 ? InitializationResult::kSuccess
212 : InitializationResult::kFailure;
213 UmaHistogramEnumeration("Android.InputHintChecker.InitializationResult", r);
214 }
215 init_state_.store(new_state, std::memory_order_release);
216 }
217
RunOffThreadInitialization()218 void InputHintChecker::RunOffThreadInitialization() {
219 JNIEnv* env = AttachCurrentThread();
220 InitGlobalRefsAndMethodIds(env);
221 DetachFromVM();
222 }
223
InitGlobalRefsAndMethodIds(JNIEnv * env)224 void InputHintChecker::InitGlobalRefsAndMethodIds(JNIEnv* env) {
225 // Obtain j.l.reflect.Method using View.class.getMethod("probablyHasInput",
226 // "...").
227 jclass view_class = env->GetObjectClass(view_class_.obj());
228 if (ClearException(env)) {
229 LOG(ERROR) << "exception on GetObjectClass(view)";
230 TransitionToState(InitState::kFailedToInitialize);
231 return;
232 }
233 jmethodID get_method_id = env->GetMethodID(
234 view_class, "getMethod",
235 "(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;");
236 if (ClearException(env)) {
237 LOG(ERROR) << "exception when looking for method getMethod()";
238 TransitionToState(InitState::kFailedToInitialize);
239 return;
240 }
241 ScopedJavaLocalRef<jstring> has_input_string =
242 ConvertUTF8ToJavaString(env, "probablyHasInput");
243 auto method = ScopedJavaLocalRef<jobject>::Adopt(
244 env, env->CallObjectMethod(view_class_.obj(), get_method_id,
245 has_input_string.obj(), nullptr));
246 if (ClearException(env)) {
247 LOG(ERROR) << "exception when calling getMethod(probablyHasInput)";
248 TransitionToState(InitState::kFailedToInitialize);
249 return;
250 }
251 if (!method) {
252 LOG(ERROR) << "got null from getMethod(probablyHasInput)";
253 TransitionToState(InitState::kFailedToInitialize);
254 return;
255 }
256
257 // Cache useful members for further calling Method.invoke(view).
258 reflect_method_for_has_input_ = ScopedJavaGlobalRef<jobject>(method);
259 jclass method_class =
260 env->GetObjectClass(reflect_method_for_has_input_.obj());
261 if (ClearException(env) || !method_class) {
262 LOG(ERROR) << "exception on GetObjectClass(getMethod) or null returned";
263 TransitionToState(InitState::kFailedToInitialize);
264 return;
265 }
266 invoke_id_ = env->GetMethodID(
267 method_class, "invoke",
268 "(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;");
269 if (ClearException(env)) {
270 LOG(ERROR) << "exception when looking for invoke() of getMethod()";
271 TransitionToState(InitState::kFailedToInitialize);
272 return;
273 }
274 jclass boolean_class = env->FindClass("java/lang/Boolean");
275 if (ClearException(env) || !boolean_class) {
276 LOG(ERROR) << "exception when looking for class Boolean or null returned";
277 TransitionToState(InitState::kFailedToInitialize);
278 return;
279 }
280 boolean_value_id_ = env->GetMethodID(boolean_class, "booleanValue", "()Z");
281 if (ClearException(env)) {
282 LOG(ERROR) << "exception when looking for method booleanValue";
283 TransitionToState(InitState::kFailedToInitialize);
284 return;
285 }
286
287 // Publish the obtained members to the thread observing kInitialized.
288 TransitionToState(InitState::kInitialized);
289 }
290
GetInstance()291 InputHintChecker& InputHintChecker::GetInstance() {
292 static NoDestructor<InputHintChecker> checker;
293 if (g_test_instance) {
294 return *g_test_instance;
295 }
296 return *checker.get();
297 }
298
ScopedOverrideInstance(InputHintChecker * checker)299 InputHintChecker::ScopedOverrideInstance::ScopedOverrideInstance(
300 InputHintChecker* checker) {
301 g_test_instance = checker;
302 }
303
~ScopedOverrideInstance()304 InputHintChecker::ScopedOverrideInstance::~ScopedOverrideInstance() {
305 g_test_instance = nullptr;
306 }
307
308 // static
RecordInputHintResult(InputHintResult result)309 void InputHintChecker::RecordInputHintResult(InputHintResult result) {
310 UMA_HISTOGRAM_ENUMERATION("Android.InputHintChecker.InputHintResult", result);
311 }
312
JNI_InputHintChecker_SetView(_JNIEnv * env,const jni_zero::JavaParamRef<jobject> & v)313 void JNI_InputHintChecker_SetView(_JNIEnv* env,
314 const jni_zero::JavaParamRef<jobject>& v) {
315 InputHintChecker::GetInstance().SetView(env, v);
316 }
317
JNI_InputHintChecker_OnCompositorViewHolderTouchEvent(_JNIEnv * env)318 void JNI_InputHintChecker_OnCompositorViewHolderTouchEvent(_JNIEnv* env) {
319 auto& checker = InputHintChecker::GetInstance();
320 if (checker.is_after_input_yield()) {
321 InputHintChecker::RecordInputHintResult(
322 InputHintResult::kCompositorViewTouchEvent);
323 }
324 checker.set_is_after_input_yield(false);
325 }
326
JNI_InputHintChecker_IsInitializedForTesting(_JNIEnv * env)327 jboolean JNI_InputHintChecker_IsInitializedForTesting(_JNIEnv* env) {
328 return InputHintChecker::GetInstance().IsInitializedForTesting(); // IN-TEST
329 }
330
JNI_InputHintChecker_FailedToInitializeForTesting(_JNIEnv * env)331 jboolean JNI_InputHintChecker_FailedToInitializeForTesting(_JNIEnv* env) {
332 return InputHintChecker::GetInstance()
333 .FailedToInitializeForTesting(); // IN-TEST
334 }
335
JNI_InputHintChecker_HasInputForTesting(_JNIEnv * env)336 jboolean JNI_InputHintChecker_HasInputForTesting(_JNIEnv* env) {
337 InputHintChecker& checker = InputHintChecker::GetInstance();
338 return checker.HasInputImplNoThrottlingForTesting(env); // IN-TEST
339 }
340
JNI_InputHintChecker_HasInputWithThrottlingForTesting(_JNIEnv * env)341 jboolean JNI_InputHintChecker_HasInputWithThrottlingForTesting(_JNIEnv* env) {
342 InputHintChecker& checker = InputHintChecker::GetInstance();
343 return checker.HasInputImplWithThrottlingForTesting(env); // IN-TEST
344 }
345
JNI_InputHintChecker_SetIsAfterInputYieldForTesting(_JNIEnv * env,jboolean after)346 void JNI_InputHintChecker_SetIsAfterInputYieldForTesting( // IN-TEST
347 _JNIEnv* env,
348 jboolean after) {
349 InputHintChecker::GetInstance().set_is_after_input_yield(after);
350 }
351
352 } // namespace base::android
353