• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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